From 756f0fce5a008d9fcb9b6bfc06a20db80db2fbb4 Mon Sep 17 00:00:00 2001 From: Jacky Li Date: Fri, 2 May 2025 18:34:11 -0700 Subject: [PATCH 1/6] feat: rapid variant edit toggle DEVSU-2620 --- .../RapidVariantEditDialog/index.tsx | 272 +++++++++++++----- .../components/RapidSummary/index.tsx | 2 + 2 files changed, 204 insertions(+), 70 deletions(-) diff --git a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx index 5681cc002..55d0df520 100644 --- a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx +++ b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx @@ -23,51 +23,165 @@ import { Box } from '@mui/system'; import { RapidVariantType } from '../../types'; import { getVariantRelevanceDict } from '../../utils'; -const KbMatchesTable = ({ kbMatches, onDelete }: { - kbMatches: KbMatchType[], - onDelete: (ident: string) => void; -}) => { - const handleKbMatchDelete = useCallback((ident) => () => { - if (onDelete && ident) { - onDelete(ident); +const condenseMatches = (matches: KbMatchedStatementType[]) => { + const grouped = {}; + + matches.forEach((item) => { + const { context, iprEvidenceLevel } = item; + + if (!grouped[context]) { + grouped[context] = {}; } - }, [onDelete]); - const kbMatchesTable = useMemo(() => { - if (!kbMatches) { return null; } - const sorted = getVariantRelevanceDict(kbMatches); - const sortedStatements = {}; - Object.entries(sorted).forEach(([relevance, matches]) => { + if (!grouped[context][iprEvidenceLevel]) { + grouped[context][iprEvidenceLevel] = []; + } + + grouped[context][iprEvidenceLevel].push(item); + }); + + return grouped; +}; + +const separateNoTable = (groupedData) => { + const noTable = {}; + const hasTable = {}; + + Object.entries(groupedData).forEach(([context, iprLevels]) => { + Object.entries(iprLevels).forEach(([iprLevel, entries]) => { + const noTableEntries = entries.filter( + (item) => item.kbData?.rapidReportTableTag === 'noTable', + ); + const otherEntries = entries.filter( + (item) => item.kbData?.rapidReportTableTag !== 'noTable', + ); + + if (noTableEntries.length > 0) { + if (!noTable[context]) noTable[context] = {}; + noTable[context][iprLevel] = noTableEntries; + } + + if (otherEntries.length > 0) { + if (!hasTable[context]) hasTable[context] = {}; + hasTable[context][iprLevel] = otherEntries; + } + }); + }); + + return { noTable, hasTable }; +}; + +const keepHighestIprPerContext = (groupedData) => { + const result = {}; + + Object.entries(groupedData).forEach(([context, iprMap]) => { + const iprLevels = Object.keys(iprMap); + + if (iprLevels.length === 0) return; + + const [highest] = iprLevels.sort(); + + result[context] = { + [highest]: iprMap[highest], + }; + }); + + return result; +}; + +type KbMatchesTableProps = { + kbMatches: KbMatchType[]; + onDelete: (idents: string[]) => void; +}; + +const KbMatchesTable = ({ kbMatches, onDelete }: KbMatchesTableProps) => { + const [editingMatches, setEditingMatches] = useState(kbMatches); + useEffect(() => { + if (kbMatches) { + setEditingMatches(kbMatches); + } + }, [kbMatches]); + + const sortedStatements = useMemo(() => { + if (!editingMatches) { + return null; + } + + const variantRelDict = getVariantRelevanceDict(editingMatches); + const sorted = {}; + Object.entries(variantRelDict).forEach(([relevance, matches]) => { matches.forEach((match) => { for (const statement of match.kbMatchedStatements) { - if (!sortedStatements[relevance]) { - sortedStatements[relevance] = [statement]; - } else if (!sortedStatements[relevance].some((item: KbMatchedStatementType) => item.ident === statement.ident)) { - sortedStatements[relevance].push(statement); + if (!sorted[relevance]) { + sorted[relevance] = [statement]; + } else if (!sorted[relevance].some((item: KbMatchedStatementType) => item.ident === statement.ident)) { + sorted[relevance].push(statement); } } }); }); + return sorted; + }, [editingMatches]); + + const handleKbMatchesToggle = useCallback((idents) => () => { + if (onDelete && idents.length) { + onDelete(idents); + } + }, [onDelete]); + + const kbMatchesInnerTable = useMemo(() => { + if (!sortedStatements) { return null; } return Object.entries(sortedStatements) - .sort(([relevance1], [relevance2]) => (relevance1 > relevance2 ? 1 : -1)) - .map(([relevance, matches]: [relevance: string, matches: KbMatchedStatementType[]]) => ( - - {relevance} - - { - matches.map((match) => ( - } - onDelete={handleKbMatchDelete(match.ident)} - /> - )) - } - - - )); - }, [kbMatches, handleKbMatchDelete]); + .sort(([relevance1], [relevance2]) => (relevance1 > relevance2 ? 1 : -1)) // Sorts by relevance alphabetically + .map(([relevance, matches]: [relevance: string, matches: KbMatchedStatementType[]]) => { + const condensedMatches = condenseMatches(matches); + const { noTable, hasTable } = separateNoTable(condensedMatches); + const highest = keepHighestIprPerContext(hasTable); + return ( + + + {relevance} + + { + Object.entries(highest).map(([key, val]) => { + const flattenedEntry = Object.values(val).flat(); + const [firstFlatEntry] = flattenedEntry; + const idents = flattenedEntry.map((stmt) => stmt.ident); + + return ( + } + onDelete={handleKbMatchesToggle(idents)} + /> + ); + }) + } + + + + + { + Object.entries(noTable).map(([key, val]) => Object.entries(val).map(([iprLevel, statements]) => { + const idents = statements.map(({ ident }) => ident); + return ( + } + onDelete={handleKbMatchesToggle(idents)} + sx={{ '& .MuiChip-label': { textDecoration: 'line-through' } }} + /> + ); + })) + } + + + + ); + }); + }, [sortedStatements, handleKbMatchesToggle]); return ( @@ -80,7 +194,7 @@ const KbMatchesTable = ({ kbMatches, onDelete }: { - {kbMatchesTable} + {kbMatchesInnerTable} @@ -101,6 +215,7 @@ enum FIELDS { interface VariantEditDialogProps extends DialogProps { editData: RapidVariantType & { potentialClinicalAssociation?: string }; + rapidVariantTableType: KbMatchedStatementType['kbData']['rapidReportTableTag']; onClose: (newData: boolean) => void; fields?: Array; } @@ -109,6 +224,7 @@ const RapidVariantEditDialog = ({ onClose, open, editData, + rapidVariantTableType, fields = [FIELDS.comments], }: VariantEditDialogProps) => { const { report } = useContext(ReportContext); @@ -118,6 +234,9 @@ const RapidVariantEditDialog = ({ const [editDataDirty, setEditDataDirty] = useState(false); const [isApiCalling, setIsApiCalling] = useState(false); + const [noTableSet] = useState(new Set()); + const [tableTypeSet] = useState(new Set()); + useEffect(() => { if (editData) { setData(editData); @@ -136,19 +255,6 @@ const RapidVariantEditDialog = ({ } }, [editDataDirty]); - const handleKbMatchDelete = useCallback((kbMatchStatementId) => { - const updatedKbMatches = data?.kbMatches.map((kbMatch: KbMatchType) => { - const updatedStatements = kbMatch.kbMatchedStatements.filter(({ ident }) => kbMatchStatementId !== ident); - return { ...kbMatch, kbMatchedStatements: updatedStatements }; - }); - handleDataChange({ - target: { - value: updatedKbMatches, - name: 'kbMatches', - }, - }); - }, [data?.kbMatches, handleDataChange]); - const handleSave = useCallback(async () => { if (editDataDirty) { setIsApiCalling(true); @@ -171,21 +277,16 @@ const RapidVariantEditDialog = ({ } if (fields.includes(FIELDS.kbMatches) && data?.kbMatches && editData?.kbMatches) { - const initialIdsSet = new Set( - editData.kbMatches.flatMap((match) => match.kbMatchedStatements.map(({ ident }) => ident)), - ); - const initialIds = Array.from(initialIdsSet); - - const remainingIdsSet = new Set(); - - for (const kbMatch of data.kbMatches) { - for (const { ident } of kbMatch.kbMatchedStatements) { - remainingIdsSet.add(ident); - } - } - - const idsToDelete = initialIds.filter((initId) => !remainingIdsSet.has(initId)); - idsToDelete.forEach((stmtId) => calls.push(api.del(`/reports/${report.ident}/kb-matches/kb-matched-statements/${stmtId}`, {}))); + noTableSet.forEach((ident) => { + calls.push(api.put(`/reports/${report.ident}/kb-matches/kb-matched-statements/${ident}`, { + kbData: { rapidReportTableTag: 'noTable' }, + })); + }); + tableTypeSet.forEach((ident) => { + calls.push(api.put(`/reports/${report.ident}/kb-matches/kb-matched-statements/${ident}`, { + kbData: { rapidReportTableTag: rapidVariantTableType }, + })); + }); } const callSet = new ApiCallSet(calls); @@ -203,18 +304,49 @@ const RapidVariantEditDialog = ({ } else { onClose(null); } - }, [editDataDirty, data, fields, isSigned, report.ident, editData?.kbMatches, showConfirmDialog, onClose]); + }, [editDataDirty, tableTypeSet, noTableSet, data?.ident, data?.potentialClinicalAssociation, data?.comments, data?.kbMatches, data?.variantType, fields, editData?.kbMatches, isSigned, report.ident, rapidVariantTableType, showConfirmDialog, onClose]); const handleDialogClose = useCallback(() => onClose(null), [onClose]); - const kbMatchesField = () => { + const handleKbMatchToggle = useCallback((kbMatchStatementIds) => { + // Find all kbMatches with that ident + const updatedKbMatches = data?.kbMatches.map((kbMatch: KbMatchType) => { + const statementsToToggle = kbMatch.kbMatchedStatements + .map((stmt) => { + if (!kbMatchStatementIds.includes(stmt.ident)) { + return stmt; + } + const nextStmt = stmt; + const { rapidReportTableTag } = nextStmt.kbData; + if (rapidReportTableTag !== 'noTable') { + nextStmt.kbData.rapidReportTableTag = 'noTable'; + noTableSet.add(stmt.ident); + tableTypeSet.delete(stmt.ident); + } else { + nextStmt.kbData.rapidReportTableTag = rapidVariantTableType; + noTableSet.delete(stmt.ident); + tableTypeSet.add(stmt.ident); + } + return nextStmt; + }); + return { ...kbMatch, kbMatchedStatements: statementsToToggle }; + }); + handleDataChange({ + target: { + value: updatedKbMatches, + name: 'kbMatches', + }, + }); + }, [data?.kbMatches, handleDataChange, noTableSet, rapidVariantTableType, tableTypeSet]); + + const kbMatchesField = useMemo(() => { if (!fields.includes(FIELDS.kbMatches)) { return null; } return ( - + ); - }; + }, [data?.kbMatches, fields, handleKbMatchToggle]); return ( @@ -222,7 +354,7 @@ const RapidVariantEditDialog = ({ Edit Event - {kbMatchesField()} + {kbMatchesField} Date: Mon, 5 May 2025 17:21:43 -0700 Subject: [PATCH 2/6] feat: processPotentialClinAssc more generic to IPR levels DEVSU-2620 --- .../components/RapidSummary/index.tsx | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/app/views/ReportView/components/RapidSummary/index.tsx b/app/views/ReportView/components/RapidSummary/index.tsx index 75679ae27..3e852835a 100644 --- a/app/views/ReportView/components/RapidSummary/index.tsx +++ b/app/views/ReportView/components/RapidSummary/index.tsx @@ -64,36 +64,26 @@ const splitIprEvidenceLevels = (kbMatches: KbMatchType[]) => { const processPotentialClinicalAssociation = (variant: RapidVariantType) => Object.entries(getVariantRelevanceDict(variant.kbMatches)) .map(([relevanceKey, kbMatches]) => { const iprEvidenceDict = splitIprEvidenceLevels(kbMatches); - if (!iprEvidenceDict['IPR-A']) { - iprEvidenceDict['IPR-A'] = new Set(); - } - if (!iprEvidenceDict['IPR-B']) { - iprEvidenceDict['IPR-B'] = new Set(); - } - const iprAArr = Array.from(iprEvidenceDict['IPR-A']); - const iprBArr = Array.from(iprEvidenceDict['IPR-B']); + const sortedIprKeys = Object.keys(iprEvidenceDict).sort( + (a, b) => a.localeCompare(b), + ); - let iprAlist = []; - if (iprAArr.length > 0) { - iprAlist = orderBy( - iprAArr, - [(cont) => cont[0].toLowerCase()], - ).map((drugName) => `${drugName} (IPR-A)`); - } + const drugToLevel = new Map(); - let iprBlist = []; - if (iprBArr.length > 0) { - iprBlist = orderBy( - iprBArr, - [(cont) => cont[0].toLowerCase()], - ).filter((drugName) => !iprEvidenceDict['IPR-A'].has(drugName)).map((drugName) => `${drugName} (IPR-B)`); + for (const iprLevel of sortedIprKeys) { + const drugs = iprEvidenceDict[iprLevel]; + for (const drug of drugs) { + if (!drugToLevel.has(drug)) { + drugToLevel.set(drug, iprLevel); + } + } } - const combinedDrugList = [ - ...iprAlist, - ...iprBlist, - ].join(', '); + const combinedDrugList = [...drugToLevel.entries()] + .map(([drug, level]) => `${drug} (${level})`) + .sort() + .join(', '); return ({ ...variant, From 95a49d9dbdfb2512a15347ae73190db9d0db5d7c Mon Sep 17 00:00:00 2001 From: Jacky Li Date: Tue, 6 May 2025 15:21:54 -0700 Subject: [PATCH 3/6] feat: update display of rapidsummary therapy list - sort by highest IPR levels, then alphabetically - updated the sort to be not just A and B, but all IPR levels DEVSU-2634 --- app/common.d.ts | 1 + .../RapidVariantEditDialog/index.tsx | 20 +++++++++++++++++-- .../components/RapidSummary/index.tsx | 19 +++++++++++++++--- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/common.d.ts b/app/common.d.ts index b606f4cb7..2c04b9569 100644 --- a/app/common.d.ts +++ b/app/common.d.ts @@ -179,6 +179,7 @@ type KbMatchedStatementType = { inferred: boolean; recruitment_status: string; kbmatchTag: string | null; + rapidReportTableTag: 'therapeutic' | 'cancerRelevance' | 'unknownSig' | 'noTable'; } | null; kbMatches: T[]; kbStatementId: string; diff --git a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx index 55d0df520..44dc38a3d 100644 --- a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx +++ b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx @@ -89,6 +89,19 @@ const keepHighestIprPerContext = (groupedData) => { return result; }; +const sortByIprThenName = ([keyA, valA], [keyB, valB]) => { + const [iprA] = Object.keys(valA); + const [iprB] = Object.keys(valB); + + const [, levelA] = iprA.split('-'); + const [, levelB] = iprB.split('-'); + + const levelCmp = levelA.localeCompare(levelB); + if (levelCmp !== 0) return levelCmp; + + return keyA.toLowerCase().localeCompare(keyB.toLowerCase()); +}; + type KbMatchesTableProps = { kbMatches: KbMatchType[]; onDelete: (idents: string[]) => void; @@ -141,9 +154,10 @@ const KbMatchesTable = ({ kbMatches, onDelete }: KbMatchesTableProps) => { {relevance} + Shown { - Object.entries(highest).map(([key, val]) => { + Object.entries(highest).sort(sortByIprThenName).map(([key, val]) => { const flattenedEntry = Object.values(val).flat(); const [firstFlatEntry] = flattenedEntry; const idents = flattenedEntry.map((stmt) => stmt.ident); @@ -161,9 +175,10 @@ const KbMatchesTable = ({ kbMatches, onDelete }: KbMatchesTableProps) => { + Not Shown { - Object.entries(noTable).map(([key, val]) => Object.entries(val).map(([iprLevel, statements]) => { + Object.entries(noTable).sort(sortByIprThenName).map(([key, val]) => Object.entries(val).map(([iprLevel, statements]) => { const idents = statements.map(({ ident }) => ident); return ( { Relevance + Drugs diff --git a/app/views/ReportView/components/RapidSummary/index.tsx b/app/views/ReportView/components/RapidSummary/index.tsx index 3e852835a..bd1f870c7 100644 --- a/app/views/ReportView/components/RapidSummary/index.tsx +++ b/app/views/ReportView/components/RapidSummary/index.tsx @@ -61,9 +61,18 @@ const splitIprEvidenceLevels = (kbMatches: KbMatchType[]) => { return iprRelevanceDict; }; -const processPotentialClinicalAssociation = (variant: RapidVariantType) => Object.entries(getVariantRelevanceDict(variant.kbMatches)) +const filterNoTable = (kbMatches: KbMatchType[]) => kbMatches.map(({ kbMatchedStatements, ...rest }) => ({ + ...rest, + kbMatchedStatements: kbMatchedStatements.filter( + ({ kbData }) => !kbData || kbData.rapidReportTableTag !== 'noTable', + ), +})); + +const processPotentialClinicalAssociation = (variant: RapidVariantType) => Object.entries( + getVariantRelevanceDict(variant.kbMatches), +) .map(([relevanceKey, kbMatches]) => { - const iprEvidenceDict = splitIprEvidenceLevels(kbMatches); + const iprEvidenceDict = splitIprEvidenceLevels(filterNoTable(kbMatches)); const sortedIprKeys = Object.keys(iprEvidenceDict).sort( (a, b) => a.localeCompare(b), @@ -81,8 +90,12 @@ const processPotentialClinicalAssociation = (variant: RapidVariantType) => Objec } const combinedDrugList = [...drugToLevel.entries()] + .sort(([drugA, levelA], [drugB, levelB]) => { + const levelCmp = levelA.localeCompare(levelB); + if (levelCmp !== 0) return levelCmp; + return drugA[0].localeCompare(drugB[0]); // compare first letter only + }) .map(([drug, level]) => `${drug} (${level})`) - .sort() .join(', '); return ({ From b98775ea4ea42845cd90642cd7cb3957cbcd2e49 Mon Sep 17 00:00:00 2001 From: Jacky Li Date: Wed, 7 May 2025 13:25:06 -0700 Subject: [PATCH 4/6] bugfix: rapidsumm/variantedit account for mut variantType DEVSU-2620 --- .../components/RapidVariantEditDialog/index.tsx | 7 +++++-- app/views/ReportView/components/RapidSummary/types.d.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx index 44dc38a3d..ec642debc 100644 --- a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx +++ b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx @@ -222,6 +222,7 @@ const VARIANT_TYPE_TO_API_MAP = { cnv: 'copy-variants', mut: 'small-mutations', sv: 'structural-variants', + tmb: 'tmbur_mutation_burden', }; enum FIELDS { @@ -283,7 +284,7 @@ const RapidVariantEditDialog = ({ const calls = []; - if (fields.includes(FIELDS.comments) && data?.comments) { + if (fields.includes(FIELDS.comments)) { calls.push(api.put( `/reports/${report.ident}/${VARIANT_TYPE_TO_API_MAP[data.variantType]}/${variantId}`, { @@ -364,6 +365,8 @@ const RapidVariantEditDialog = ({ ); }, [data?.kbMatches, fields, handleKbMatchToggle]); + const disableCommentField = !fields.includes(FIELDS.comments) || editData?.variantType === 'tmb'; + return ( @@ -378,7 +381,7 @@ const RapidVariantEditDialog = ({ name="comments" onChange={handleDataChange} variant="outlined" - disabled={!fields.includes(FIELDS.comments)} + disabled={disableCommentField} multiline fullWidth /> diff --git a/app/views/ReportView/components/RapidSummary/types.d.ts b/app/views/ReportView/components/RapidSummary/types.d.ts index 6d68826fe..616b9a089 100644 --- a/app/views/ReportView/components/RapidSummary/types.d.ts +++ b/app/views/ReportView/components/RapidSummary/types.d.ts @@ -1,8 +1,8 @@ import { - CopyNumberType, SmallMutationType, StructuralVariantType, + CopyNumberType, SmallMutationType, StructuralVariantType, TmburType, } from '@/common'; -type RapidVariantType = CopyNumberType | SmallMutationType | StructuralVariantType; +type RapidVariantType = CopyNumberType | SmallMutationType | StructuralVariantType | TmburType; export { RapidVariantType, From e3aa3c49d176c2e4dde7bc673260df007f9ad3fe Mon Sep 17 00:00:00 2001 From: Jacky Li Date: Thu, 8 May 2025 13:25:16 -0700 Subject: [PATCH 5/6] feat: add signature type to disable comment field DEVSU-2620 --- .../RapidSummary/components/RapidVariantEditDialog/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx index ec642debc..b7752077e 100644 --- a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx +++ b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx @@ -365,7 +365,7 @@ const RapidVariantEditDialog = ({ ); }, [data?.kbMatches, fields, handleKbMatchToggle]); - const disableCommentField = !fields.includes(FIELDS.comments) || editData?.variantType === 'tmb'; + const disableCommentField = !fields.includes(FIELDS.comments) || ['tmb', 'signature'].includes(editData?.variantType); return ( From a266428ce5a55fd752ef6d92434d331e43e21b3a Mon Sep 17 00:00:00 2001 From: Jacky Li Date: Thu, 8 May 2025 13:31:03 -0700 Subject: [PATCH 6/6] bugfix: add check to comments calls for rapid when variantType isn't accounted for DEVSU-2620 --- .../components/RapidVariantEditDialog/index.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx index b7752077e..3f8aa8222 100644 --- a/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx +++ b/app/views/ReportView/components/RapidSummary/components/RapidVariantEditDialog/index.tsx @@ -222,7 +222,6 @@ const VARIANT_TYPE_TO_API_MAP = { cnv: 'copy-variants', mut: 'small-mutations', sv: 'structural-variants', - tmb: 'tmbur_mutation_burden', }; enum FIELDS { @@ -285,12 +284,16 @@ const RapidVariantEditDialog = ({ const calls = []; if (fields.includes(FIELDS.comments)) { - calls.push(api.put( - `/reports/${report.ident}/${VARIANT_TYPE_TO_API_MAP[data.variantType]}/${variantId}`, - { - comments: data.comments, - }, - )); + const variantLink = VARIANT_TYPE_TO_API_MAP[data.variantType]; + // TODO: Once API finalizes remove this check + if (variantLink) { + calls.push(api.put( + `/reports/${report.ident}/${variantLink}/${variantId}`, + { + comments: data.comments, + }, + )); + } } if (fields.includes(FIELDS.kbMatches) && data?.kbMatches && editData?.kbMatches) {