Skip to content

Commit d26c69b

Browse files
authored
Gene Tea Terms Modal Copy Genes Button (#516)
1 parent 322e49e commit d26c69b

8 files changed

Lines changed: 262 additions & 60 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.CopyListButton {
2+
display: flex;
3+
align-items: center;
4+
gap: 8px;
5+
6+
.copyButton {
7+
cursor: pointer;
8+
}
9+
10+
.successMessage {
11+
color: #2e7d32;
12+
display: flex;
13+
align-items: center;
14+
font-size: 14px;
15+
font-weight: 500;
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { useState } from "react";
2+
import { Button } from "react-bootstrap";
3+
import styles from "./CopyListButton.scss";
4+
5+
interface CopyButtonProps {
6+
items: string[];
7+
title: string;
8+
disabled: boolean;
9+
}
10+
11+
const CopyListButton: React.FC<CopyButtonProps> = ({
12+
items,
13+
title,
14+
disabled,
15+
}) => {
16+
const [showSuccess, setShowSuccess] = useState(false);
17+
18+
const handleCopy = async () => {
19+
try {
20+
const listString = items.join(",");
21+
await navigator.clipboard.writeText(listString);
22+
23+
// Show success message
24+
setShowSuccess(true);
25+
26+
// Hide message after 3 seconds
27+
setTimeout(() => {
28+
setShowSuccess(false);
29+
}, 3000);
30+
} catch (err) {
31+
console.error("Failed to copy: ", err);
32+
}
33+
};
34+
35+
return (
36+
<div className={styles.CopyListButton}>
37+
<Button
38+
onClick={handleCopy}
39+
bsStyle={"secondary"}
40+
className={styles.copyButton}
41+
disabled={disabled}
42+
>
43+
{title}
44+
</Button>
45+
46+
{showSuccess && (
47+
<span className={styles.successMessage}>
48+
{/* Green Check Mark SVG */}
49+
<svg
50+
width="18"
51+
height="18"
52+
viewBox="0 0 24 24"
53+
fill="none"
54+
stroke="currentColor"
55+
strokeWidth="3"
56+
strokeLinecap="round"
57+
strokeLinejoin="round"
58+
style={{ marginRight: "4px" }}
59+
>
60+
<polyline points="20 6 9 17 4 12" />
61+
</svg>
62+
Copied genes to clipboard!
63+
</span>
64+
)}
65+
</div>
66+
);
67+
};
68+
69+
export default CopyListButton;

frontend/packages/portal-frontend/src/geneTea/components/TopTermsTab/FindGenesMatchingTerm/Modal/ExcerptTable/ExcerptTable.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import React from "react";
1+
import React, { useMemo } from "react";
22
import { Spinner } from "@depmap/common-components";
33
import GeneTeaTerm from "@depmap/data-explorer-2/src/components/DataExplorerPage/components/plot/integrations/GeneTea/GeneTeaTerm";
44
import { Alert, Button } from "react-bootstrap";
55
import { useExcerptData } from "../../../../../hooks/useExcerptData";
66
import PaginationControls from "./PaginationControls";
77
import styles from "../../../../../styles/GeneTea.scss";
8+
import CopyListButton from "../CopyListButton/CopyListButton";
9+
import { useFetchGeneList } from "src/geneTea/hooks/useFetchGeneList";
810

911
interface ExcerptTableProps {
1012
useTerms: boolean;
@@ -19,8 +21,12 @@ const ExcerptTable: React.FC<ExcerptTableProps> = ({
1921
termToMatchingGenesMap,
2022
useAllGenes,
2123
}: ExcerptTableProps) => {
24+
const termToMatchingGenesObj = useMemo(() => {
25+
return Object.fromEntries(termToMatchingGenesMap);
26+
}, [termToMatchingGenesMap]);
27+
2228
const {
23-
isLoading,
29+
isLoading: isDataLoading,
2430
error,
2531
pageData,
2632
totalPages,
@@ -32,8 +38,15 @@ const ExcerptTable: React.FC<ExcerptTableProps> = ({
3238
pageSize,
3339
} = useExcerptData(term, termToMatchingGenesMap, useAllGenes);
3440

35-
const isContentReady = pageData && !error && !isLoading;
36-
const isTableVisible = isContentReady && Object.keys(pageData!).length > 0;
41+
const { geneList, isLoading: isListLoading } = useFetchGeneList(
42+
useTerms,
43+
term,
44+
[term],
45+
termToMatchingGenesObj,
46+
useAllGenes
47+
);
48+
49+
const isLoading = isDataLoading || isListLoading;
3750

3851
const renderTableBody = () => {
3952
if (!pageData) return null;
@@ -76,10 +89,17 @@ const ExcerptTable: React.FC<ExcerptTableProps> = ({
7689
<Button
7790
bsStyle="primary"
7891
bsSize="small"
92+
disabled={isLoading}
7993
onClick={handleClickCreateTermContext}
8094
>
81-
Save Term as Gene Context
95+
{isLoading ? "Loading..." : "Save Term as Gene Context"}
8296
</Button>
97+
<CopyListButton
98+
key="excerpt-table-copy-button"
99+
items={geneList}
100+
title={"Copy Gene List"}
101+
disabled={isLoading || geneList.length === 0}
102+
/>
83103
</div>
84104
)}
85105

@@ -90,7 +110,7 @@ const ExcerptTable: React.FC<ExcerptTableProps> = ({
90110
<th>Excerpt</th>
91111
</tr>
92112
</thead>
93-
{isTableVisible && renderTableBody()}
113+
{renderTableBody()}
94114
</table>
95115

96116
{isLoading && (

frontend/packages/portal-frontend/src/geneTea/components/TopTermsTab/FindGenesMatchingTerm/Modal/MatchingTermsModal.tsx

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import styles from "../../../../styles/GeneTea.scss";
55
import ExcerptTable from "./ExcerptTable/ExcerptTable";
66
import TermGroupTabs from "./TermGroupTabs";
77
import { useGeneContextCreation } from "../../../../hooks/useCreateGeneContext";
8+
import CopyListButton from "./CopyListButton/CopyListButton";
9+
import { useFetchGeneList } from "src/geneTea/hooks/useFetchGeneList";
810

911
interface Props {
1012
termOrTermGroup: string;
@@ -25,28 +27,34 @@ function MatchingTermsModal({
2527
}: Props) {
2628
const [show, setShow] = useState(true);
2729

28-
const handleContextSaveComplete = useCallback(() => setShow(true), []);
30+
const termToMatchingGenesObj = useMemo(() => {
31+
return Object.fromEntries(termToMatchingGenesMap);
32+
}, [termToMatchingGenesMap]);
2933

30-
// --- 1. SINGLE TERM CONTEXT CREATION ---
31-
const handleClickCreateTermContext = useGeneContextCreation({
32-
name: termOrTermGroup,
33-
terms: useTerms ? [termOrTermGroup] : [],
34-
termToMatchingGenesMap,
35-
useAllGenes,
36-
onComplete: handleContextSaveComplete,
37-
});
34+
const termsKey = useMemo(() => {
35+
const terms = useTerms ? [termOrTermGroup] : termsWithinSelectedGroup || [];
36+
return terms.join(",");
37+
}, [useTerms, termOrTermGroup, termsWithinSelectedGroup]);
3838

39-
// --- 2. TERM GROUP CONTEXT CREATION ---
40-
const handleClickCreateTermGroupContext = useGeneContextCreation({
39+
const { geneList, isLoading } = useFetchGeneList(
40+
useTerms,
41+
termOrTermGroup,
42+
termsWithinSelectedGroup,
43+
termToMatchingGenesObj,
44+
useAllGenes
45+
);
46+
47+
const handleContextSaveComplete = useCallback(() => setShow(true), []);
48+
49+
const handleClickCreateContext = useGeneContextCreation({
4150
name: termOrTermGroup,
42-
terms: termsWithinSelectedGroup || [], // Pass all terms in the group
43-
termToMatchingGenesMap,
51+
termsKey,
52+
termToMatchingGenesObj,
4453
useAllGenes,
4554
onComplete: handleContextSaveComplete,
4655
});
4756

4857
const modalBody = useMemo(() => {
49-
// If not grouping terms
5058
if (useTerms || termsWithinSelectedGroup?.length === 1) {
5159
return (
5260
<ExcerptTable
@@ -58,7 +66,6 @@ function MatchingTermsModal({
5866
);
5967
}
6068

61-
// If grouping terms
6269
if (!termsWithinSelectedGroup || termsWithinSelectedGroup.length === 0) {
6370
return <div>No terms found for this group.</div>;
6471
}
@@ -85,20 +92,20 @@ function MatchingTermsModal({
8592
<Modal.Title>Excerpts for “{termOrTermGroup}</Modal.Title>
8693
</Modal.Header>
8794
<Modal.Body className={styles.GeneTeaModal}>{modalBody}</Modal.Body>
88-
<Modal.Footer>
95+
<Modal.Footer className={styles.modalFooterRow}>
8996
<Button onClick={onClose}>Close</Button>
9097
<Button
9198
bsStyle="primary"
92-
onClick={
93-
useTerms
94-
? handleClickCreateTermContext
95-
: handleClickCreateTermGroupContext
96-
}
99+
disabled={isLoading}
100+
onClick={handleClickCreateContext}
97101
>
98-
{useTerms
99-
? "Save as Gene Context"
100-
: `Save Term Group as Gene Context`}
102+
{useTerms ? "Save as Gene Context" : "Save Group as Gene Context"}
101103
</Button>
104+
<CopyListButton
105+
items={geneList}
106+
title={"Copy Gene List"}
107+
disabled={isLoading}
108+
/>
102109
</Modal.Footer>
103110
</Modal>
104111
);
Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
import { useCallback } from "react";
22
import { DepMap } from "@depmap/globals";
3-
import { cached, legacyPortalAPI } from "@depmap/api";
3+
import { fetchGeneList } from "./utils";
44

55
interface GeneContextCreationParams {
6-
name: string; // termOrTermGroup
7-
terms: string[]; // List of terms to fetch genes for
8-
termToMatchingGenesMap: Map<string, string[]>;
6+
name: string;
7+
termsKey: string; // "term1,term2,term3"
8+
termToMatchingGenesObj: Record<string, string[]>;
99
useAllGenes: boolean;
1010
onComplete: () => void;
1111
}
1212

13-
/**
14-
* Utility function to consolidate gene fetching and context saving logic.
15-
*/
1613
const saveContext = async (
1714
contextName: string,
1815
geneList: string[],
@@ -30,34 +27,22 @@ const saveContext = async (
3027

3128
export const useGeneContextCreation = ({
3229
name,
33-
terms,
34-
termToMatchingGenesMap,
30+
termsKey,
31+
termToMatchingGenesObj,
3532
useAllGenes,
3633
onComplete,
3734
}: GeneContextCreationParams) => {
3835
return useCallback(async () => {
39-
let finalGenes: string[] = [];
36+
const finalGenes = await fetchGeneList(
37+
termsKey,
38+
termToMatchingGenesObj,
39+
useAllGenes
40+
);
4041

41-
if (useAllGenes) {
42-
// 1. Fetch genes from API
43-
const allGenesData = await cached(
44-
legacyPortalAPI
45-
).fetchGeneTeaGenesMatchingTermExperimental(terms, []);
46-
47-
// 2. Process fetched genes
48-
finalGenes = Object.keys(allGenesData).flatMap(
49-
(term) => allGenesData[term]?.split(" ") || []
50-
);
51-
} else if (terms.length === 1) {
52-
finalGenes = termToMatchingGenesMap.get(terms[0]) || [];
53-
} else {
54-
// 2. If grouping terms
55-
finalGenes = Array.from(
56-
new Set(terms.flatMap((term) => termToMatchingGenesMap.get(term) || []))
57-
);
42+
if (finalGenes.length > 0) {
43+
await saveContext(name, finalGenes, onComplete);
5844
}
5945

60-
// 3. Save the context
61-
await saveContext(name, finalGenes, onComplete);
62-
}, [name, terms, termToMatchingGenesMap, useAllGenes, onComplete]);
46+
return finalGenes;
47+
}, [name, termsKey, termToMatchingGenesObj, useAllGenes, onComplete]);
6348
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useEffect, useState } from "react";
2+
import { fetchGeneList } from "./utils";
3+
4+
export const useFetchGeneList = (
5+
useTerms: boolean,
6+
termOrTermGroup: string,
7+
termsArray: string[] | null,
8+
termToMatchingGenesObj: Record<string, string[]>,
9+
useAllGenes: boolean
10+
) => {
11+
const [geneList, setGeneList] = useState<string[]>([]);
12+
const [isLoading, setIsLoading] = useState<boolean>(false);
13+
14+
const termsKey = useTerms ? termOrTermGroup : termsArray?.join(",") || "";
15+
16+
useEffect(() => {
17+
const loadGenes = async () => {
18+
if (!termsKey) {
19+
setGeneList([]);
20+
return;
21+
}
22+
23+
setIsLoading(true);
24+
try {
25+
const finalGenes = await fetchGeneList(
26+
termsKey,
27+
termToMatchingGenesObj,
28+
useAllGenes
29+
);
30+
setGeneList(finalGenes);
31+
} catch (error) {
32+
console.error(error);
33+
} finally {
34+
setIsLoading(false);
35+
}
36+
};
37+
38+
loadGenes();
39+
}, [termsKey, termToMatchingGenesObj, useAllGenes]);
40+
41+
return { geneList, isLoading };
42+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { cached, legacyPortalAPI } from "@depmap/api";
2+
3+
export const fetchGeneList = async (
4+
termsString: string,
5+
termToMatchingGenesObj: Record<string, string[]>,
6+
useAllGenes: boolean
7+
): Promise<string[]> => {
8+
const terms = termsString ? termsString.split(",") : [];
9+
10+
if (terms.length === 0) return [];
11+
12+
if (useAllGenes) {
13+
const allGenesData = await cached(
14+
legacyPortalAPI
15+
).fetchGeneTeaGenesMatchingTermExperimental(terms, []);
16+
17+
return Object.keys(allGenesData).flatMap(
18+
(term) => allGenesData[term]?.split(" ") || []
19+
);
20+
}
21+
22+
if (terms.length === 1) {
23+
return termToMatchingGenesObj[terms[0]] || [];
24+
}
25+
26+
return Array.from(
27+
new Set(terms.flatMap((term) => termToMatchingGenesObj[term] || []))
28+
);
29+
};

0 commit comments

Comments
 (0)