Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DC-857: hierarchy view stub removal #4730

Merged
merged 14 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/components/TreeGrid.test.ts
Original file line number Diff line number Diff line change
@@ -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 & {
Expand All @@ -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 },
Expand All @@ -39,6 +39,17 @@ const columns = [
{ name: 'col3', width: 100, render: col3 },
];

const parents: Parent<Node>[] = [
{
parentId: 0,
children: [root],
},
{
parentId: root.id,
children: [child1, child2],
},
];

describe('TreeGrid', () => {
let getChildrenCount: number;
const renderTree = () => {
Expand All @@ -47,6 +58,7 @@ describe('TreeGrid', () => {
TreeGrid({
columns,
root: rootPointer,
parents,
getChildren: async (node) => {
getChildrenCount++;
const id = node.id;
Expand All @@ -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' },
Expand Down
17 changes: 11 additions & 6 deletions src/components/TreeGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends RowContents> = {
Expand All @@ -26,6 +24,11 @@ export type Column<T extends RowContents> = {
render: (row: T) => string | ReactElement;
};

export type Parent<T extends RowContents> = {
readonly parentId: number;
readonly children: T[];
};

type RowState = 'closed' | 'opening' | 'open';

type Row<T extends RowContents> = {
Expand All @@ -48,10 +51,10 @@ const wrapContent =
state: 'closed',
});

export const populateTreeFromRoot = <T extends RowContents>(root: T): Row<T>[] => {
export const populateTree = <T extends RowContents>(root: T, parents: Parent<T>[]): Row<T>[] => {
const createRows = (parent: T, depth: number, previousRows: Row<T>[]): Row<T>[] => {
// does parent have children?
const children = parent.children ?? [];
const children = _.find({ parentId: parent.id }, parents)?.children ?? [];
const parentRow: Row<T> = {
contents: parent,
depth,
Expand All @@ -72,6 +75,8 @@ type TreeGridProps<T extends RowContents> = {
readonly columns: Column<T>[];
/** 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<T>[];
pshapiro4broad marked this conversation as resolved.
Show resolved Hide resolved
/** Given a row, return its children. This is only called if row.hasChildren is true. */
readonly getChildren: (row: T) => Promise<T[]>;
/** Optional header style */
Expand Down Expand Up @@ -110,8 +115,8 @@ const getRowIndex = <T extends RowContents>(row: Row<T>, rows: Row<T>[]) =>
_.findIndex((r) => r.contents.id === row.contents.id, rows);

const TreeGridInner = <T extends RowContents>(props: TreeGridPropsInner<T>) => {
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<T>) => {
const index = getRowIndex(row, data);
Expand Down
17 changes: 9 additions & 8 deletions src/dataset-builder/ConceptSelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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();
});
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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<DataRepoContract['dataset']>),
} as Partial<DataRepoContract> as DataRepoContract;
asMockedFn(DataRepo).mockImplementation(() => mockDataRepoContract as DataRepoContract);
Expand All @@ -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) =>
({
Expand Down
9 changes: 5 additions & 4 deletions src/dataset-builder/ConceptSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,7 +17,7 @@ type ConceptSelectorProps = {
readonly actionText: string;
readonly datasetId: string;
readonly initialCart: Concept[];
readonly rootConcept: Concept;
readonly parents: Parent<Concept>[];
readonly openedConcept?: Concept;
};

Expand All @@ -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<Concept[]>(initialCart);
const getChildren = async (concept: Concept): Promise<Concept[]> => {
Expand Down Expand Up @@ -83,7 +83,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 },
pshapiro4broad marked this conversation as resolved.
Show resolved Hide resolved
parents,
getChildren,
headerStyle: tableHeaderStyle,
}),
Expand Down
10 changes: 6 additions & 4 deletions src/dataset-builder/ConceptSetCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ export const ConceptSetCreator = (props: ConceptSetCreatorProps) => {
};
return h(ConceptSelector, {
// Concept selection currently only supports top level domains, so nodes should not be expandable
rootConcept: {
...domainOptionRoot,
children: _.map((child) => ({ ...child, hasChildren: false }), domainOptionRoot.children),
},
parents: [
{
parentId: domainOptionRoot.id,
children: _.map((child) => ({ ...child, hasChildren: false }), domainOptionRoot.children),
},
],
initialCart: cart,
title: 'Add concept',
onCancel: () => onStateChange(homepageState.new()),
Expand Down
7 changes: 6 additions & 1 deletion src/dataset-builder/DatasetBuilderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,13 @@ export interface GetConceptsResponse {
result: Concept[];
}

export interface SnapshotBuilderParentConcept {
parentId: number;
children: Concept[];
}

export interface GetConceptHierarchyResponse {
result: Concept;
readonly result: SnapshotBuilderParentConcept[];
}

export interface SearchConceptsResponse {
Expand Down
2 changes: 1 addition & 1 deletion src/dataset-builder/DomainCriteriaSelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataRepoContract['dataset']>),
} as Partial<DataRepoContract> as DataRepoContract;
const datasetId = '';
Expand Down
7 changes: 4 additions & 3 deletions src/dataset-builder/DomainCriteriaSelector.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -59,7 +60,7 @@ export const saveSelected =

export const DomainCriteriaSelector = (props: DomainCriteriaSelectorProps) => {
const { state, onStateChange, datasetId, getNextCriteriaIndex } = props;
const [hierarchy, setHierarchy] = useLoadedData<Concept>();
const [hierarchy, setHierarchy] = useLoadedData<Parent<Concept>[]>();
useOnMount(() => {
const openedConcept = state.openedConcept;
if (openedConcept) {
Expand All @@ -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,
Expand Down
Loading