From 00a3783c575cddf55eb4c01cb5da861e7284fb84 Mon Sep 17 00:00:00 2001 From: Phil Shapiro Date: Wed, 20 Mar 2024 14:43:56 -0400 Subject: [PATCH 1/6] DC-857: hierarchy view stub removal --- src/components/TreeGrid.ts | 17 +++++++++++------ src/dataset-builder/ConceptSelector.ts | 9 +++++---- src/dataset-builder/ConceptSetCreator.ts | 11 ++--------- src/dataset-builder/DatasetBuilderUtils.ts | 7 ++++++- src/dataset-builder/DomainCriteriaSelector.ts | 7 ++++--- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/components/TreeGrid.ts b/src/components/TreeGrid.ts index 5a09e8d221..8839f4c6b9 100644 --- a/src/components/TreeGrid.ts +++ b/src/components/TreeGrid.ts @@ -13,8 +13,6 @@ export type RowContents = { id: number; /** If true, this row has children. */ hasChildren: boolean; - /** Used when initially populating the tree. If present, this contains all of a node's children. */ - children?: RowContents[]; }; export type Column = { @@ -26,6 +24,11 @@ export type Column = { render: (row: T) => string | ReactElement; }; +export type Parent = { + readonly parentId: number; + readonly children: T[]; +}; + type RowState = 'closed' | 'opening' | 'open'; type Row = { @@ -48,10 +51,10 @@ const wrapContent = state: 'closed', }); -export const populateTreeFromRoot = (root: T): Row[] => { +export const populateTree = (root: T, parents: Parent[]): Row[] => { const createRows = (parent: T, depth: number, previousRows: Row[]): Row[] => { // does parent have children? - const children = parent.children ?? []; + const children = _.find({ parentId: parent.id }, parents)?.children ?? []; const parentRow: Row = { contents: parent, depth, @@ -72,6 +75,8 @@ type TreeGridProps = { readonly columns: Column[]; /** The root of the tree to display. Note that the root node is hidden, and only its children are shown. */ readonly root: T; + /** For a pre-initialized tree, all the known parent nodes. */ + readonly parents?: Parent[]; /** Given a row, return its children. This is only called if row.hasChildren is true. */ readonly getChildren: (row: T) => Promise; /** Optional header style */ @@ -110,8 +115,8 @@ const getRowIndex = (row: Row, rows: Row[]) => _.findIndex((r) => r.contents.id === row.contents.id, rows); const TreeGridInner = (props: TreeGridPropsInner) => { - const { columns, getChildren, gridWidth, root } = props; - const [data, setData] = useState(populateTreeFromRoot(root)); + const { columns, getChildren, gridWidth, root, parents } = props; + const [data, setData] = useState(populateTree(root, parents ?? [])); const rowHeight = 40; const expand = async (row: Row) => { const index = getRowIndex(row, data); diff --git a/src/dataset-builder/ConceptSelector.ts b/src/dataset-builder/ConceptSelector.ts index 901a667817..242e45a3cd 100644 --- a/src/dataset-builder/ConceptSelector.ts +++ b/src/dataset-builder/ConceptSelector.ts @@ -5,7 +5,7 @@ import { div, h, h2 } from 'react-hyperscript-helpers'; import { ActionBar } from 'src/components/ActionBar'; import { Link } from 'src/components/common'; import { icon } from 'src/components/icons'; -import { TreeGrid } from 'src/components/TreeGrid'; +import { Parent, TreeGrid } from 'src/components/TreeGrid'; import { BuilderPageHeader } from 'src/dataset-builder/DatasetBuilderHeader'; import { DataRepo, SnapshotBuilderConcept as Concept } from 'src/libs/ajax/DataRepo'; import colors from 'src/libs/colors'; @@ -17,7 +17,7 @@ type ConceptSelectorProps = { readonly actionText: string; readonly datasetId: string; readonly initialCart: Concept[]; - readonly rootConcept: Concept; + readonly parents: Parent[]; readonly openedConcept?: Concept; }; @@ -32,7 +32,7 @@ export const tableHeaderStyle: CSSProperties = { }; export const ConceptSelector = (props: ConceptSelectorProps) => { - const { title, onCancel, onCommit, actionText, datasetId, initialCart, rootConcept, openedConcept } = props; + const { title, onCancel, onCommit, actionText, datasetId, initialCart, parents, openedConcept } = props; const [cart, setCart] = useState(initialCart); const getChildren = async (concept: Concept): Promise => { @@ -78,7 +78,8 @@ export const ConceptSelector = (props: ConceptSelectorProps) => { { name: 'Concept ID', width: 195, render: _.get('id') }, { name: 'Roll-up count', width: 205, render: _.get('count') }, ], - root: rootConcept, + root: { id: parents[0].parentId, name: 'root', count: 0, hasChildren: true }, + parents, getChildren, headerStyle: tableHeaderStyle, }), diff --git a/src/dataset-builder/ConceptSetCreator.ts b/src/dataset-builder/ConceptSetCreator.ts index 28e647de69..db9f47752c 100644 --- a/src/dataset-builder/ConceptSetCreator.ts +++ b/src/dataset-builder/ConceptSetCreator.ts @@ -23,16 +23,9 @@ export const toConceptSet = (concept: Concept): ConceptSet => { export const ConceptSetCreator = (props: ConceptSetCreatorProps) => { const { onStateChange, dataset, conceptSetUpdater } = props; const { snapshotBuilderSettings, id } = dataset; - // create a root for all domainOptions - const domainOptionRoot: Concept = { - id: 0, - name: 'Point to Domain Options', - count: 100, - hasChildren: true, - children: _.map(_.get('root'), snapshotBuilderSettings?.domainOptions), - }; return h(ConceptSelector, { - rootConcept: domainOptionRoot, + // create a root for all domainOptions + parents: [{ parentId: 0, children: [_.map(_.get('root'), snapshotBuilderSettings?.domainOptions)] }], initialCart: [], title: 'Add concept', onCancel: () => onStateChange(homepageState.new()), diff --git a/src/dataset-builder/DatasetBuilderUtils.ts b/src/dataset-builder/DatasetBuilderUtils.ts index e52668349e..b60fceb37a 100644 --- a/src/dataset-builder/DatasetBuilderUtils.ts +++ b/src/dataset-builder/DatasetBuilderUtils.ts @@ -132,8 +132,13 @@ export interface GetConceptsResponse { result: Concept[]; } +export interface SnapshotBuilderParentConcept { + parentId: number; + children: Concept[]; +} + export interface GetConceptHierarchyResponse { - result: Concept; + readonly result: SnapshotBuilderParentConcept[]; } export interface SearchConceptsResponse { diff --git a/src/dataset-builder/DomainCriteriaSelector.ts b/src/dataset-builder/DomainCriteriaSelector.ts index bdb1a61d9a..62f600b9b4 100644 --- a/src/dataset-builder/DomainCriteriaSelector.ts +++ b/src/dataset-builder/DomainCriteriaSelector.ts @@ -1,6 +1,7 @@ import _ from 'lodash/fp'; import { h } from 'react-hyperscript-helpers'; import { spinnerOverlay } from 'src/components/common'; +import { Parent } from 'src/components/TreeGrid'; import { DomainCriteria } from 'src/dataset-builder/DatasetBuilderUtils'; import { DataRepo, @@ -59,7 +60,7 @@ export const saveSelected = export const DomainCriteriaSelector = (props: DomainCriteriaSelectorProps) => { const { state, onStateChange, datasetId, getNextCriteriaIndex } = props; - const [hierarchy, setHierarchy] = useLoadedData(); + const [hierarchy, setHierarchy] = useLoadedData[]>(); useOnMount(() => { const openedConcept = state.openedConcept; if (openedConcept) { @@ -70,14 +71,14 @@ export const DomainCriteriaSelector = (props: DomainCriteriaSelectorProps) => { // get the children of this concept void setHierarchy(async () => { const results = (await DataRepo().dataset(datasetId).getConcepts(state.domainOption.root)).result; - return { ...state.domainOption.root, children: results }; + return [{ parentId: state.domainOption.root.id, children: results }]; }); } }); return hierarchy.status === 'Ready' ? h(ConceptSelector, { - rootConcept: hierarchy.state, + parents: hierarchy.state, domainOptionRoot: state.domainOption.root, title: state.domainOption.name, initialCart: state.cart, From f198f0b8f9d218345fa0484594b4b97a5098f3ee Mon Sep 17 00:00:00 2001 From: Phil Shapiro Date: Mon, 25 Mar 2024 14:08:25 -0400 Subject: [PATCH 2/6] Fix tests --- src/components/TreeGrid.test.ts | 20 +++++++++++++++---- src/dataset-builder/ConceptSelector.test.ts | 17 ++++++++-------- .../DomainCriteriaSelector.test.ts | 2 +- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/components/TreeGrid.test.ts b/src/components/TreeGrid.test.ts index 15315a31e0..93cecc5c6c 100644 --- a/src/components/TreeGrid.test.ts +++ b/src/components/TreeGrid.test.ts @@ -1,7 +1,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import _ from 'lodash/fp'; -import { populateTreeFromRoot, RowContents, TreeGrid } from 'src/components/TreeGrid'; +import { Parent, populateTree, RowContents, TreeGrid } from 'src/components/TreeGrid'; import { renderWithAppContexts as render } from 'src/testing/test-utils'; type Node = RowContents & { @@ -12,8 +12,8 @@ type Node = RowContents & { const child1: Node = { id: 2, name: 'child1', hasChildren: false }; const child2: Node = { id: 3, name: 'child2', hasChildren: true }; const child3: Node = { id: 4, name: 'child3', hasChildren: false }; -const root: Node = { id: 1, name: 'root', hasChildren: true, children: [child1, child2] }; -const rootPointer: Node = { id: 0, name: 'Point to Root', hasChildren: true, children: [root] }; +const root: Node = { id: 1, name: 'root', hasChildren: true }; +const rootPointer: Node = { id: 0, name: 'Point to Root', hasChildren: true }; const testConcepts = [ { id: 0, name: 'Point to Root', hasChildren: true }, @@ -39,6 +39,17 @@ const columns = [ { name: 'col3', width: 100, render: col3 }, ]; +const parents: Parent[] = [ + { + parentId: 0, + children: [root], + }, + { + parentId: root.id, + children: [child1, child2], + }, +]; + describe('TreeGrid', () => { let getChildrenCount: number; const renderTree = () => { @@ -47,6 +58,7 @@ describe('TreeGrid', () => { TreeGrid({ columns, root: rootPointer, + parents, getChildren: async (node) => { getChildrenCount++; const id = node.id; @@ -59,7 +71,7 @@ describe('TreeGrid', () => { }; it('initializes the tree with nodes in the correct orrder', () => { - expect(populateTreeFromRoot(rootPointer)).toEqual([ + expect(populateTree(rootPointer, parents)).toEqual([ { contents: root, depth: 0, isFetched: true, state: 'open' }, { contents: child1, depth: 1, isFetched: false, state: 'closed' }, { contents: child2, depth: 1, isFetched: false, state: 'closed' }, diff --git a/src/dataset-builder/ConceptSelector.test.ts b/src/dataset-builder/ConceptSelector.test.ts index 76f2b342c0..65be9ae0e8 100644 --- a/src/dataset-builder/ConceptSelector.test.ts +++ b/src/dataset-builder/ConceptSelector.test.ts @@ -23,12 +23,12 @@ describe('ConceptSelector', () => { const actionText = 'action text'; const datasetId = '0'; // Using 101 so the ID doesn't match the count. - const rootConcept = { ...dummyGetConceptForId(100), children: [dummyGetConceptForId(101)] }; + const parents = [{ parentId: 0, children: [dummyGetConceptForId(101), dummyGetConceptForId(102)] }]; const renderSelector = (initialCart: SnapshotBuilderConcept[] = []) => { render( h(ConceptSelector, { actionText, - rootConcept, + parents, initialCart, onCancel, onCommit, @@ -38,8 +38,8 @@ describe('ConceptSelector', () => { ); }; - const firstChild = rootConcept.children[0]; - const secondChild = rootConcept.children[1]; + const firstChild = parents[0].children[0]; + const secondChild = parents[0].children[1]; it('renders the concept selector', () => { // Arrange @@ -48,7 +48,8 @@ describe('ConceptSelector', () => { expect(screen.queryByText(title)).toBeTruthy(); expect(screen.queryByText(firstChild.name)).toBeTruthy(); expect(screen.queryByText(firstChild.id)).toBeTruthy(); - expect(screen.queryByText(firstChild.count || 0)).toBeTruthy(); + // Two elements have the same count. + expect(screen.queryAllByText(firstChild.count!)).toHaveLength(2); // Action text not visible until a row is selected. expect(screen.queryByText(actionText)).toBeFalsy(); }); @@ -122,7 +123,7 @@ describe('ConceptSelector', () => { await user.click(screen.getByLabelText(`add ${firstChild.id}`)); await user.click(screen.getByText(actionText)); // Assert - expect(onCommit).toHaveBeenCalledWith(rootConcept.children); + expect(onCommit).toHaveBeenCalledWith([firstChild]); }); it('calls cancel on cancel', async () => { @@ -142,7 +143,7 @@ describe('ConceptSelector', () => { const mockDataRepoContract: DataRepoContract = { dataset: (_datasetId) => ({ - getConcepts: () => Promise.resolve({ result: [dummyGetConceptForId(102)] }), + getConcepts: () => Promise.resolve({ result: [dummyGetConceptForId(103)] }), } as Partial), } as Partial as DataRepoContract; asMockedFn(DataRepo).mockImplementation(() => mockDataRepoContract as DataRepoContract); @@ -157,7 +158,7 @@ describe('ConceptSelector', () => { it('supports multiple add to cart', async () => { // Arrange renderSelector(); - const expandConcept = dummyGetConceptForId(102); + const expandConcept = dummyGetConceptForId(103); const mockDataRepoContract: DataRepoContract = { dataset: (_datasetId) => ({ diff --git a/src/dataset-builder/DomainCriteriaSelector.test.ts b/src/dataset-builder/DomainCriteriaSelector.test.ts index 67bd0fd4f5..898bd279d4 100644 --- a/src/dataset-builder/DomainCriteriaSelector.test.ts +++ b/src/dataset-builder/DomainCriteriaSelector.test.ts @@ -29,7 +29,7 @@ describe('DomainCriteriaSelector', () => { dataset: (_datasetId) => ({ getConcepts: () => Promise.resolve({ result: [concept] }), - getConceptHierarchy: () => Promise.resolve({ result: { ...concept, children } }), + getConceptHierarchy: () => Promise.resolve({ result: [{ parentId: concept.id, children }] }), } as Partial), } as Partial as DataRepoContract; const datasetId = ''; From 69c24d23c093e86bd010b2132918f4b4d424188e Mon Sep 17 00:00:00 2001 From: Phil Shapiro Date: Tue, 26 Mar 2024 10:48:25 -0400 Subject: [PATCH 3/6] remove unnecessary `await` --- src/dataset-builder/CohortEditor.test.ts | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/dataset-builder/CohortEditor.test.ts b/src/dataset-builder/CohortEditor.test.ts index 3c864a4106..23335d2bfd 100644 --- a/src/dataset-builder/CohortEditor.test.ts +++ b/src/dataset-builder/CohortEditor.test.ts @@ -164,14 +164,14 @@ describe('CohortEditor', () => { it('renders list criteria', async () => { // Arrange mockListStatistics(); - const criteria = (await criteriaFromOption(0, { + const criteria = criteriaFromOption(0, { id: 0, name: 'list', kind: 'list', tableName: 'person', columnName: 'list_column', values: [], - })) as ProgramDataListCriteria; + }) as ProgramDataListCriteria; renderCriteriaView({ criteria }); expect(await screen.findByText(criteria.option.name, { exact: false })).toBeTruthy(); @@ -182,7 +182,7 @@ describe('CohortEditor', () => { // Arrange const user = userEvent.setup(); const updateCriteria = jest.fn(); - const criteria = (await criteriaFromOption(0, { + const criteria = criteriaFromOption(0, { id: 0, name: 'list', kind: 'list', @@ -198,7 +198,7 @@ describe('CohortEditor', () => { name: 'value 1', }, ], - })) as ProgramDataListCriteria; + }) as ProgramDataListCriteria; criteria.values = [{ id: 0, name: 'value 0' }]; renderCriteriaView({ updateCriteria, criteria }); // Act @@ -222,7 +222,7 @@ describe('CohortEditor', () => { it('renders range criteria', async () => { // Arrange - const criteria = (await criteriaFromOption(0, { + const criteria = criteriaFromOption(0, { id: 0, name: 'range', kind: 'range', @@ -230,7 +230,7 @@ describe('CohortEditor', () => { columnName: 'range_column', min: 55, max: 99, - })) as ProgramDataRangeCriteria; + }) as ProgramDataRangeCriteria; renderCriteriaView({ criteria }); // Assert expect(await screen.findByText(criteria.option.name, { exact: false })).toBeTruthy(); @@ -242,7 +242,7 @@ describe('CohortEditor', () => { // Arrange const user = userEvent.setup(); mockRangeStatistics(); - const criteria = (await criteriaFromOption(0, { + const criteria = criteriaFromOption(0, { id: 0, name: 'range', kind: 'range', @@ -250,7 +250,7 @@ describe('CohortEditor', () => { columnName: 'range_column', min: 55, max: 99, - })) as ProgramDataRangeCriteria; + }) as ProgramDataRangeCriteria; const updateCriteria = jest.fn(); renderCriteriaView({ criteria, updateCriteria }); const lowInput = 65; @@ -271,7 +271,7 @@ describe('CohortEditor', () => { const min = 55; const max = 99; - const criteria = (await criteriaFromOption(0, { + const criteria = criteriaFromOption(0, { id: 0, tableName: 'person', columnName: 'range_column', @@ -279,7 +279,7 @@ describe('CohortEditor', () => { kind: 'range', min, max, - })) as ProgramDataRangeCriteria; + }) as ProgramDataRangeCriteria; const updateCriteria = jest.fn(); renderCriteriaView({ criteria, updateCriteria }); // Act @@ -298,7 +298,7 @@ describe('CohortEditor', () => { it('can delete criteria', async () => { // Arrange - const criteria = (await criteriaFromOption(0, { + const criteria = criteriaFromOption(0, { id: 0, tableName: 'person', columnName: 'range_column', @@ -306,7 +306,7 @@ describe('CohortEditor', () => { kind: 'range', min: 55, max: 99, - })) as ProgramDataRangeCriteria; + }) as ProgramDataRangeCriteria; const deleteCriteria = jest.fn(); renderCriteriaView({ deleteCriteria, criteria }); @@ -428,13 +428,13 @@ describe('CohortEditor', () => { expect(updateCohort).toHaveBeenCalledTimes(1); const updatedCohort: Cohort = updateCohort.mock.calls[0][0](cohort); - const { index: _, ...expectedCriteria } = await criteriaFromOption(2, programDataRangeOption()); + const { index: _, ...expectedCriteria } = criteriaFromOption(2, programDataRangeOption()); expect(updatedCohort.criteriaGroups[0].criteria).toMatchObject([expectedCriteria]); }); it('can delete criteria from the criteria group', async () => { // Arrange - const criteria = await criteriaFromOption(0, programDataRangeOption()); + const criteria = criteriaFromOption(0, programDataRangeOption()); const { cohort, updateCohort } = showCriteriaGroup({ initializeGroup: (criteriaGroup) => criteriaGroup.criteria.push(criteria), }); From 8f2caffa7ca8c94e9c211c8c4713d0b879a7dad0 Mon Sep 17 00:00:00 2001 From: Phil Shapiro Date: Tue, 26 Mar 2024 13:22:55 -0400 Subject: [PATCH 4/6] remove unused field `children` from model; small cleanup to concept set code --- src/dataset-builder/ConceptSetCreator.ts | 16 ++++++---------- src/libs/ajax/DataRepo.ts | 1 - 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/dataset-builder/ConceptSetCreator.ts b/src/dataset-builder/ConceptSetCreator.ts index 755686b898..66e9d51434 100644 --- a/src/dataset-builder/ConceptSetCreator.ts +++ b/src/dataset-builder/ConceptSetCreator.ts @@ -31,20 +31,16 @@ export const toConcept = (conceptSet: DomainConceptSet): Concept => { export const ConceptSetCreator = (props: ConceptSetCreatorProps) => { const { onStateChange, dataset, conceptSetUpdater, cart } = props; const { snapshotBuilderSettings, id } = dataset; - // create a root for all domainOptions - const domainOptionRoot: Concept = { - id: 0, - name: 'Point to Domain Options', - count: 100, - hasChildren: true, - children: _.map(_.get('root'), snapshotBuilderSettings?.domainOptions), - }; return h(ConceptSelector, { + // create a root for all domainOptions // Concept selection currently only supports top level domains, so nodes should not be expandable parents: [ { - parentId: domainOptionRoot.id, - children: _.map((child) => ({ ...child, hasChildren: false }), domainOptionRoot.children), + parentId: 0, + children: _.map( + _.flow(_.get('root'), (child) => ({ ...child, hasChildren: false })), + snapshotBuilderSettings?.domainOptions + ), }, ], initialCart: cart, diff --git a/src/libs/ajax/DataRepo.ts b/src/libs/ajax/DataRepo.ts index e47f97931d..71e8ca10ea 100644 --- a/src/libs/ajax/DataRepo.ts +++ b/src/libs/ajax/DataRepo.ts @@ -18,7 +18,6 @@ export type SnapshotBuilderConcept = { name: string; count?: number; hasChildren: boolean; - children?: SnapshotBuilderConcept[]; }; export type SnapshotBuilderOptionTypeNames = 'list' | 'range' | 'domain'; From c69db50024542e0db95ce92683e9598c2d5806ed Mon Sep 17 00:00:00 2001 From: Phil Shapiro Date: Thu, 28 Mar 2024 16:56:09 -0400 Subject: [PATCH 5/6] Based on PR feedback, move the code to find the root tree node to the UI instead of the API layer --- src/dataset-builder/ConceptSelector.test.ts | 14 +++++++++++++- src/dataset-builder/ConceptSelector.ts | 12 ++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/dataset-builder/ConceptSelector.test.ts b/src/dataset-builder/ConceptSelector.test.ts index 65be9ae0e8..b3c6ae2fa9 100644 --- a/src/dataset-builder/ConceptSelector.test.ts +++ b/src/dataset-builder/ConceptSelector.test.ts @@ -5,7 +5,7 @@ import { dummyGetConceptForId } from 'src/dataset-builder/TestConstants'; import { DataRepo, DataRepoContract, SnapshotBuilderConcept } from 'src/libs/ajax/DataRepo'; import { asMockedFn, renderWithAppContexts as render } from 'src/testing/test-utils'; -import { ConceptSelector } from './ConceptSelector'; +import { ConceptSelector, findRoot } from './ConceptSelector'; jest.mock('src/libs/ajax/GoogleStorage'); type DataRepoExports = typeof import('src/libs/ajax/DataRepo'); @@ -174,4 +174,16 @@ describe('ConceptSelector', () => { // Assert expect(screen.getByText('2 concepts', { exact: false })).toBeTruthy(); }); + + it('finds the root concept', () => { + // Arrange + const parents = [ + { parentId: 101, children: [dummyGetConceptForId(102)] }, + { parentId: 0, children: [dummyGetConceptForId(101)] }, + ]; + // Act + const root = findRoot(parents); + // Assert + expect(root).toEqual(0); + }); }); diff --git a/src/dataset-builder/ConceptSelector.ts b/src/dataset-builder/ConceptSelector.ts index 6310279349..37e6b11243 100644 --- a/src/dataset-builder/ConceptSelector.ts +++ b/src/dataset-builder/ConceptSelector.ts @@ -5,7 +5,7 @@ import { div, h, h2 } from 'react-hyperscript-helpers'; import { ActionBar } from 'src/components/ActionBar'; import { Link } from 'src/components/common'; import { icon } from 'src/components/icons'; -import { Parent, TreeGrid } from 'src/components/TreeGrid'; +import { Parent, RowContents, TreeGrid } from 'src/components/TreeGrid'; import { BuilderPageHeader } from 'src/dataset-builder/DatasetBuilderHeader'; import { DataRepo, SnapshotBuilderConcept as Concept } from 'src/libs/ajax/DataRepo'; import colors from 'src/libs/colors'; @@ -31,6 +31,14 @@ export const tableHeaderStyle: CSSProperties = { border: `.5px solid ${colors.dark(0.2)}`, }; +// The list of parents is a tree, where each parent has a list of children. Find the root +// of the tree by finding the one parent that is not the child of any other parent +export const findRoot = (parents: Parent[]) => { + const childIds = new Set(_.flow(_.map('children'), _.flatten, _.map('id'))(parents)); + const root = _.filter((parent: Parent) => !childIds.has(parent.parentId))(parents); + return root[0] ? root[0].parentId : 0; +}; + export const ConceptSelector = (props: ConceptSelectorProps) => { const { title, onCancel, onCommit, actionText, datasetId, initialCart, parents, openedConcept } = props; @@ -83,7 +91,7 @@ export const ConceptSelector = (props: ConceptSelectorProps) => { { name: 'Concept ID', width: 195, render: _.get('id') }, { name: 'Roll-up count', width: 205, render: _.get('count') }, ], - root: { id: parents[0].parentId, name: 'root', count: 0, hasChildren: true }, + root: { id: findRoot(parents), name: 'root', count: 0, hasChildren: true }, parents, getChildren, headerStyle: tableHeaderStyle, From 282b761612181823af9a027dc1aae1808413279f Mon Sep 17 00:00:00 2001 From: Phil Shapiro Date: Fri, 29 Mar 2024 09:16:11 -0400 Subject: [PATCH 6/6] minor comment changes --- src/components/TreeGrid.ts | 1 + src/dataset-builder/ConceptSelector.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TreeGrid.ts b/src/components/TreeGrid.ts index 8839f4c6b9..ae85f9ab52 100644 --- a/src/components/TreeGrid.ts +++ b/src/components/TreeGrid.ts @@ -24,6 +24,7 @@ export type Column = { render: (row: T) => string | ReactElement; }; +/** A parent node in a pre-initialized tree. Each parent node contains its id and a list of its children. */ export type Parent = { readonly parentId: number; readonly children: T[]; diff --git a/src/dataset-builder/ConceptSelector.ts b/src/dataset-builder/ConceptSelector.ts index 37e6b11243..c1ac886163 100644 --- a/src/dataset-builder/ConceptSelector.ts +++ b/src/dataset-builder/ConceptSelector.ts @@ -32,7 +32,7 @@ export const tableHeaderStyle: CSSProperties = { }; // The list of parents is a tree, where each parent has a list of children. Find the root -// of the tree by finding the one parent that is not the child of any other parent +// of the tree by finding the one parent that is not the child of any other parent. export const findRoot = (parents: Parent[]) => { const childIds = new Set(_.flow(_.map('children'), _.flatten, _.map('id'))(parents)); const root = _.filter((parent: Parent) => !childIds.has(parent.parentId))(parents);