From 59ceca8baa933189c25b6fd979139862ba0fbe5a Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 17 Mar 2026 21:57:49 -0400 Subject: [PATCH 01/43] fix: update year to 2026 --- packages/ui/src/common/Accordion/AccordionItem.tsx | 2 +- packages/ui/src/common/Dropdown/Dropdown.tsx | 14 +++++++++----- packages/ui/src/theme/styles/icons.ts | 2 +- .../ui/src/viewer-table/DictionaryTableViewer.tsx | 2 +- packages/ui/src/viewer-table/index.ts | 2 +- .../viewer-table/DictionaryViewerPage.stories.tsx | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/common/Accordion/AccordionItem.tsx b/packages/ui/src/common/Accordion/AccordionItem.tsx index 11975853..63524d0d 100644 --- a/packages/ui/src/common/Accordion/AccordionItem.tsx +++ b/packages/ui/src/common/Accordion/AccordionItem.tsx @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved * * This program and the accompanying materials are made available under the terms of * the GNU Affero General Public License v3.0. You should have received a copy of the diff --git a/packages/ui/src/common/Dropdown/Dropdown.tsx b/packages/ui/src/common/Dropdown/Dropdown.tsx index 19188d13..644c54d1 100644 --- a/packages/ui/src/common/Dropdown/Dropdown.tsx +++ b/packages/ui/src/common/Dropdown/Dropdown.tsx @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved * * This program and the accompanying materials are made available under the terms of * the GNU Affero General Public License v3.0. You should have received a copy of the @@ -111,6 +111,8 @@ export type DropDownProps = { disabled?: boolean; size?: number; styles?: SerializedStyles; + children?: ReactNode; + panelStyles?: SerializedStyles; }; /** @@ -126,7 +128,7 @@ export type DropDownProps = { * @returns {JSX.Element} Dropdown component */ -const Dropdown = ({ menuItems = [], title, leftIcon, disabled = false, size = 16, styles }: DropDownProps) => { +const Dropdown = ({ menuItems = [], title, leftIcon, disabled = false, size = 16, styles, children, panelStyles }: DropDownProps) => { const [open, setOpen] = useState(false); const dropdownRef = useRef(null); const theme: Theme = useThemeContext(); @@ -179,9 +181,11 @@ const Dropdown = ({ menuItems = [], title, leftIcon, disabled = false, size = 16 {open && !disabled && ( - - {renderMenuItems()} - + children ? +
{children}
+ : + {renderMenuItems()} + )} ); diff --git a/packages/ui/src/theme/styles/icons.ts b/packages/ui/src/theme/styles/icons.ts index 6e032368..1181100e 100644 --- a/packages/ui/src/theme/styles/icons.ts +++ b/packages/ui/src/theme/styles/icons.ts @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved * * This program and the accompanying materials are made available under the terms of * the GNU Affero General Public License v3.0. You should have received a copy of the diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index 77405090..3192395a 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved * * This program and the accompanying materials are made available under the terms of * the GNU Affero General Public License v3.0. You should have received a copy of the diff --git a/packages/ui/src/viewer-table/index.ts b/packages/ui/src/viewer-table/index.ts index e303cd6a..22595426 100644 --- a/packages/ui/src/viewer-table/index.ts +++ b/packages/ui/src/viewer-table/index.ts @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved * * This program and the accompanying materials are made available under the terms of * the GNU Affero General Public License v3.0. You should have received a copy of the diff --git a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx index 96b846b0..52f93c83 100644 --- a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx +++ b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved * * This program and the accompanying materials are made available under the terms of * the GNU Affero General Public License v3.0. You should have received a copy of the From ef1d8f580287b14e72de823c958a4f62907d5938 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 17 Mar 2026 22:09:20 -0400 Subject: [PATCH 02/43] refactor: change attribute filter component from a dropdown to a button --- .../DictionaryDataContext.tsx | 2 +- packages/ui/src/index.ts | 2 +- ...Dropdown.tsx => AttributeFilterButton.tsx} | 48 +++++++------------ packages/ui/src/viewer-table/Toolbar/index.ts | 3 +- packages/ui/src/viewer-table/index.ts | 2 +- 5 files changed, 21 insertions(+), 36 deletions(-) rename packages/ui/src/viewer-table/Toolbar/{AttributeFilterDropdown.tsx => AttributeFilterButton.tsx} (62%) diff --git a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx index e1e9d6e4..4f3af8cd 100644 --- a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx +++ b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx @@ -27,7 +27,7 @@ import { fetchAndValidateHostedDictionaries, fetchRemoteDictionary } from './sou export type DictionaryServerUnion = DictionaryServerRecord | Dictionary; -export type FilterOptions = 'Required' | 'All Fields'; +export type FilterOptions = 'Required'; export type DictionaryDataContextType = { dictionaries?: DictionaryServerUnion[]; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 53451781..4e6a130c 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -37,7 +37,7 @@ export { type ToolbarProps, // & individual parts for integrators to build their own - AttributeFilterDropdown, + AttributeFilterButton, CollapseAllButton, type CollapseAllButtonProps, DictionaryDownloadButton, diff --git a/packages/ui/src/viewer-table/Toolbar/AttributeFilterDropdown.tsx b/packages/ui/src/viewer-table/Toolbar/AttributeFilterButton.tsx similarity index 62% rename from packages/ui/src/viewer-table/Toolbar/AttributeFilterDropdown.tsx rename to packages/ui/src/viewer-table/Toolbar/AttributeFilterButton.tsx index 4eb1e91a..ba11945a 100644 --- a/packages/ui/src/viewer-table/Toolbar/AttributeFilterDropdown.tsx +++ b/packages/ui/src/viewer-table/Toolbar/AttributeFilterButton.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved * * This program and the accompanying materials are made available under the terms of * the GNU Affero General Public License v3.0. You should have received a copy of the @@ -17,15 +17,11 @@ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import Dropdown from '../../common/Dropdown/index'; -import { - FilterOptions, - useDictionaryDataContext, - useDictionaryStateContext, -} from '../../dictionary-controller/DictionaryDataContext'; +import Button from '../../common/Button'; +import { useDictionaryDataContext, useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; import { type Theme, useThemeContext } from '../../theme/index'; -const AttributeFilterDropdown = () => { +const AttributeFilterButton = () => { const theme: Theme = useThemeContext(); const { loading, errors } = useDictionaryDataContext(); @@ -33,33 +29,21 @@ const AttributeFilterDropdown = () => { const { ListFilter } = theme.icons; - const handleFilterSelect = (selectedFilterName: FilterOptions) => { - if (filters?.includes(selectedFilterName)) { - setFilters([]); - return; - } - setFilters([selectedFilterName]); - }; + const isRequired = filters.includes('Required'); - const menuItems = [ - { - label: 'Required', - action: () => handleFilterSelect('Required'), - }, - { - label: 'All Fields', - action: () => handleFilterSelect('All Fields'), - }, - ]; + const handleClick = () => { + setFilters(isRequired ? [] : ['Required']); + }; return ( - } - title="Filters" - menuItems={menuItems} - disabled={errors.length > 0 || loading} - /> + ); }; -export default AttributeFilterDropdown; +export default AttributeFilterButton; diff --git a/packages/ui/src/viewer-table/Toolbar/index.ts b/packages/ui/src/viewer-table/Toolbar/index.ts index 9028ed3b..b6dd9fe3 100644 --- a/packages/ui/src/viewer-table/Toolbar/index.ts +++ b/packages/ui/src/viewer-table/Toolbar/index.ts @@ -1,4 +1,5 @@ -export { default as AttributeFilterDropdown } from './AttributeFilterDropdown.js'; +export { default as AttributeFilterButton } from './AttributeFilterButton.js'; +export { default as FilterRows, type FilterRowsProps } from './FilterRows.js'; export { default as CollapseAllButton, type CollapseAllButtonProps } from './CollapseAllButton.js'; export { default as DiagramViewButton } from './DiagramViewButton.js'; export { default as DictionaryDownloadButton, type DictionaryDownloadButtonProps } from './DictionaryDownloadButton.js'; diff --git a/packages/ui/src/viewer-table/index.ts b/packages/ui/src/viewer-table/index.ts index 22595426..9025fa79 100644 --- a/packages/ui/src/viewer-table/index.ts +++ b/packages/ui/src/viewer-table/index.ts @@ -27,7 +27,7 @@ export { type ToolbarProps, // individual parts for integrators to build their own - AttributeFilterDropdown, + AttributeFilterButton, CollapseAllButton, type CollapseAllButtonProps, DictionaryDownloadButton, From f7fb03339e2d1e4f33090da27b468c6196a39714 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 17 Mar 2026 22:10:25 -0400 Subject: [PATCH 03/43] fix: remove unnecessary function --- packages/ui/src/common/Accordion/AccordionItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/common/Accordion/AccordionItem.tsx b/packages/ui/src/common/Accordion/AccordionItem.tsx index 63524d0d..d742de76 100644 --- a/packages/ui/src/common/Accordion/AccordionItem.tsx +++ b/packages/ui/src/common/Accordion/AccordionItem.tsx @@ -111,7 +111,7 @@ const titleRowStyle = (theme: Theme) => css` } `; -const hashIconStyle = () => css` +const hashIconStyle = css` display: inline-flex; align-items: center; justify-content: center; @@ -196,7 +196,7 @@ const AccordionItem = ({ index, accordionData, openState }: AccordionItemProps) + : } {relationshipMap && !loading && errors.length === 0 && ( From 7224591dcb8f55f2503f531a35cc4307bcde5a5c Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 17 Mar 2026 22:14:34 -0400 Subject: [PATCH 07/43] feat: add new filterrows component for rendering in multiple filters --- .../src/viewer-table/Toolbar/FilterRows.tsx | 105 ++++++++++++++++++ .../ui/src/viewer-table/Toolbar/Toolbar.tsx | 30 ++--- 2 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 packages/ui/src/viewer-table/Toolbar/FilterRows.tsx diff --git a/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx b/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx new file mode 100644 index 00000000..e862caf5 --- /dev/null +++ b/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx @@ -0,0 +1,105 @@ +/* + * + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; + +import { useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; +import { type Theme, useThemeContext } from '../../theme/index'; +import type { CustomFilterCategory } from '../DictionaryTableViewer'; + +const sectionHeaderStyle = (theme: Theme) => css` + ${theme.typography.buttonText}; + font-size: 13px; + color: ${theme.colors.accent_dark}; + padding: 6px 12px; + border-top: 1px solid ${theme.colors.border_button}; + border-bottom: 1px solid ${theme.colors.border_button}; + text-transform: uppercase; + letter-spacing: 0.5px; +`; + +const categoryGroupStyle = css` + &:first-of-type > div:first-of-type { + border-top: none; + } +`; + +const checkboxRowStyle = (theme: Theme) => css` + display: flex; + align-items: center; + gap: 8px; + padding: 4px 12px; + cursor: pointer; + &:hover { + background-color: ${theme.colors.accent_1}; + } + input[type='checkbox'] { + accent-color: #3a8736; + } +`; + +const checkboxLabelStyle = (theme: Theme) => css` + ${theme.typography.body}; + color: ${theme.colors.accent_dark}; + cursor: pointer; + user-select: none; +`; + +export type FilterRowsProps = { + categories: CustomFilterCategory[]; +}; + +const FilterRows = ({ categories }: FilterRowsProps) => { + const theme = useThemeContext(); + const { customFilterSelections, toggleCustomFilter } = useDictionaryStateContext(); + + const hasMultipleCategories = categories.length > 1; + + return ( + <> + {categories.map((category) => { + const selected = customFilterSelections[category.filterProperty] ?? []; + return ( +
+ {hasMultipleCategories &&
{category.label} Filter
} + {category.options.map((option) => { + const isChecked = selected.includes(option); + return ( + + ); + })} +
+ ); + })} + + ); +}; + +export default FilterRows; diff --git a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx index 83086586..7b55cfe5 100644 --- a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx +++ b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx @@ -26,10 +26,11 @@ import { css } from '@emotion/react'; import { useDictionaryDataContext, useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; import { type Theme, useThemeContext } from '../../theme/index'; import { ToolbarSkeleton } from '../Loading'; +import type { CustomFilterCategory } from '../DictionaryTableViewer'; import Dropdown from '../../common/Dropdown/index'; -import type { ToolbarCustomDropdown } from '../DictionaryTableViewer'; -import AttributeFilterDropdown from './AttributeFilterDropdown'; +import AttributeFilterButton from './AttributeFilterButton'; +import FilterRows from './FilterRows'; import CollapseAllButton from './CollapseAllButton'; import DiagramViewButton from './DiagramViewButton'; import DictionaryDownloadButton from './DictionaryDownloadButton'; @@ -40,7 +41,7 @@ export type ToolbarProps = { onSelect: (schemaNameIndex: number) => void; setIsCollapsed: (collapsed: boolean) => void; isCollapsed: boolean; - customFilterDropdowns?: ToolbarCustomDropdown[]; + customFilterCategories?: CustomFilterCategory[]; }; const panelStyles = (theme: Theme) => css` @@ -64,9 +65,14 @@ const sectionStyles = css` gap: 16px; `; -const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, customFilterDropdowns }: ToolbarProps) => { +const customFilterPanelStyles = css` + min-width: 200px; + max-height: 300px; +`; + +const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, customFilterCategories }: ToolbarProps) => { const theme: Theme = useThemeContext(); - const { loading } = useDictionaryDataContext(); + const { loading, errors } = useDictionaryDataContext(); const { selectedDictionary } = useDictionaryStateContext(); if (!selectedDictionary && !loading) { @@ -82,20 +88,6 @@ const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, customFilterDropdowns
- {customFilterDropdowns && - customFilterDropdowns.map((dropdown) => ( - dropdown.onSelect(undefined) }, - ...dropdown.options.map((option) => ({ - label: option, - action: () => dropdown.onSelect(option), - })), - ]} - /> - ))} {isCollapsed ? setIsCollapsed(false)} /> : setIsCollapsed(true)} />} From 4dfc168cff24e15788f6cc166f29e041d4f75343 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 17 Mar 2026 22:15:18 -0400 Subject: [PATCH 08/43] refactor: update and add new story for single custom filter --- packages/ui/stories/dictionaryDecorator.tsx | 36 +++++++++---------- .../DictionaryViewerPage.stories.tsx | 11 ++++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/ui/stories/dictionaryDecorator.tsx b/packages/ui/stories/dictionaryDecorator.tsx index 4c2f35fa..296ce368 100644 --- a/packages/ui/stories/dictionaryDecorator.tsx +++ b/packages/ui/stories/dictionaryDecorator.tsx @@ -125,24 +125,24 @@ export const withErrorState = (): Decorator => { ); }; -const schemaMetaMap: Record = { - participant: { category: 'Clinical', tier: 'Required' }, - sociodemographic: { category: 'Clinical', tier: 'Optional' }, - demographic: { category: 'Clinical', tier: 'Required' }, - diagnosis: { category: 'Clinical', tier: 'Required' }, - treatment: { category: 'Clinical', tier: 'Optional' }, - follow_up: { category: 'Clinical', tier: 'Optional' }, - procedure: { category: 'Clinical', tier: 'Optional' }, - medication: { category: 'Clinical', tier: 'Optional' }, - radiation: { category: 'Clinical', tier: 'Optional' }, - measurement: { category: 'Clinical', tier: 'Optional' }, - phenotype: { category: 'Clinical', tier: 'Optional' }, - comorbidity: { category: 'Clinical', tier: 'Optional' }, - exposure: { category: 'Clinical', tier: 'Optional' }, - specimen: { category: 'Biospecimen', tier: 'Required' }, - sample: { category: 'Biospecimen', tier: 'Required' }, - experiment: { category: 'Genomic', tier: 'Required' }, - read_group: { category: 'Genomic', tier: 'Optional' }, +const schemaMetaMap: Record = { + participant: { submitter: 'Clinician', domain: 'Health' }, + sociodemographic: { submitter: 'Clinician', domain: 'Health' }, + demographic: { submitter: 'Clinician', domain: 'Health' }, + diagnosis: { submitter: 'Clinician', domain: 'Clinical' }, + treatment: { submitter: 'Clinician', domain: 'Clinical' }, + follow_up: { submitter: 'Clinician', domain: 'Clinical' }, + procedure: { submitter: 'Clinician', domain: 'Clinical' }, + medication: { submitter: 'Laboratory', domain: 'Health' }, + radiation: { submitter: 'Laboratory', domain: 'Health' }, + measurement: { submitter: 'Laboratory', domain: 'Clinical' }, + phenotype: { submitter: 'Researcher', domain: 'Clinical' }, + comorbidity: { submitter: 'Researcher', domain: 'Clinical' }, + exposure: { submitter: 'Researcher', domain: 'Clinical' }, + specimen: { submitter: 'Laboratory', domain: 'Health' }, + sample: { submitter: 'Laboratory', domain: 'Clinical' }, + experiment: { submitter: 'Researcher', domain: 'Clinical' }, + read_group: { submitter: 'Researcher', domain: 'Clinical' }, }; const customFilterDictionaryData: DictionaryTestData = [ diff --git a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx index 52f93c83..56ab390c 100644 --- a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx +++ b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx @@ -95,12 +95,19 @@ export const LecternServer: Story = { decorators: [withLecternUrl()], }; +export const WithSingleCustomFilter: Story = { + decorators: [withCustomFilterDictionary], + args: { + customFilterDropdowns: [{ label: 'Submitter', filterProperty: 'meta.submitter' }], + }, +}; + export const WithCustomFilterDropdowns: Story = { decorators: [withCustomFilterDictionary], args: { customFilterDropdowns: [ - { label: 'Category', filterProperty: 'meta.category' }, - { label: 'Tier', filterProperty: 'meta.tier' }, + { label: 'Submitter', filterProperty: 'meta.submitter' }, + { label: 'Domain', filterProperty: 'meta.domain' }, ], }, }; From 9ba08ab0e04c0e4c78acf69ea984ef377f67bc99 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 17 Mar 2026 22:26:27 -0400 Subject: [PATCH 09/43] fix: get rid of extra line --- packages/ui/src/viewer-table/Toolbar/FilterRows.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx b/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx index e862caf5..92cd0687 100644 --- a/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx +++ b/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx @@ -72,7 +72,6 @@ export type FilterRowsProps = { const FilterRows = ({ categories }: FilterRowsProps) => { const theme = useThemeContext(); const { customFilterSelections, toggleCustomFilter } = useDictionaryStateContext(); - const hasMultipleCategories = categories.length > 1; return ( From eddf209d164b09a201679fc3fcad71c7b338e95f Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 17 Mar 2026 22:28:04 -0400 Subject: [PATCH 10/43] fix: define icon separately --- packages/ui/src/viewer-table/Toolbar/Toolbar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx index 7b55cfe5..9dd9c9c4 100644 --- a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx +++ b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx @@ -74,6 +74,7 @@ const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, customFilterCategories const theme: Theme = useThemeContext(); const { loading, errors } = useDictionaryDataContext(); const { selectedDictionary } = useDictionaryStateContext(); + const { ListFilter } = theme.icons; if (!selectedDictionary && !loading) { return null; @@ -94,7 +95,7 @@ const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, customFilterCategories {customFilterCategories && customFilterCategories.length > 0 && ( } + leftIcon={} title={customFilterCategories.length === 1 ? customFilterCategories[0].label : 'Filters'} disabled={loading || errors.length > 0} panelStyles={customFilterPanelStyles} From f4b66cb670e5c29e49deacc2bdc979bec6326319 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 18 Mar 2026 12:43:52 -0400 Subject: [PATCH 11/43] refactor: rename custom filter catergories to filter catergories --- .../ui/src/viewer-table/DictionaryTableViewer.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index 0225b1e4..a552646a 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -26,6 +26,7 @@ import type { Dictionary, Schema, SchemaFieldRestrictions } from '@overture-stac import { useCallback, useEffect, useMemo, useState } from 'react'; import Accordion from '../common/Accordion/index'; +import type { FilterCategory } from '../common/Dropdown/index'; import Modal from '../common/Modal'; import { ErrorModal } from '../common/Error/ErrorModal'; import { useDictionaryDataContext, useDictionaryStateContext } from '../dictionary-controller/DictionaryDataContext'; @@ -51,12 +52,6 @@ export type CustomFilterDropdown = { filterProperty: string; }; -export type CustomFilterCategory = { - label: string; - filterProperty: string; - options: string[]; -}; - export type DictionaryTableViewerProps = { customFilterDropdowns?: CustomFilterDropdown[]; }; @@ -260,7 +255,7 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable [selectedDictionary], ); - const customFilterCategories: CustomFilterCategory[] | undefined = useMemo(() => { + const filterCategories: FilterCategory[] | undefined = useMemo(() => { if (!customFilterDropdowns?.length || !selectedDictionary?.schemas) { return undefined; } @@ -409,7 +404,7 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable onSelect={handleAccordionSelect} setIsCollapsed={setIsCollapsed} isCollapsed={isCollapsed} - customFilterCategories={customFilterCategories} + filterCategories={filterCategories} /> {accordionItems.length === 0 && activeFilters.length > 0 ?
From f3e8dc3136ded0a2f83318b257806f764c38b712 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 18 Mar 2026 12:45:56 -0400 Subject: [PATCH 12/43] refactor: dropdown now renders children instead of handling rendering --- packages/ui/src/common/Dropdown/Dropdown.tsx | 53 ++++++++------------ 1 file changed, 21 insertions(+), 32 deletions(-) diff --git a/packages/ui/src/common/Dropdown/Dropdown.tsx b/packages/ui/src/common/Dropdown/Dropdown.tsx index 644c54d1..c32fcc24 100644 --- a/packages/ui/src/common/Dropdown/Dropdown.tsx +++ b/packages/ui/src/common/Dropdown/Dropdown.tsx @@ -26,8 +26,6 @@ import { type MouseEvent as ReactMouseEvent, type ReactNode, useCallback, useEff import { type Theme, useThemeContext } from '../../theme/index'; -import DropDownItem from './DropdownItem'; - const disabledButtonStyle = css` cursor: not-allowed; opacity: 0.7; @@ -94,41 +92,42 @@ const dropdownMenuStyle = (theme: Theme) => css` z-index: 100; max-height: 150px; overflow-y: auto; - list-style: none; margin: 0; padding: 0; `; -type MenuItem = { - label: string; - action: () => void; -}; - export type DropDownProps = { title?: string; leftIcon?: ReactNode; - menuItems?: MenuItem[]; disabled?: boolean; size?: number; styles?: SerializedStyles; children?: ReactNode; - panelStyles?: SerializedStyles; + closeOnSelect?: boolean; }; /** * Dropdown component with toggle button and collapsible menu. * - * @param {DropdownProps} props - Dropdown configuration - * @param {DropDownProps} title - Text displayed on the dropdown button - * @param {DropDownProps} leftIcon - Custom icon displayed on the left side - * @param {DropDownProps} menuItems - Array of menu items with label and action - * @param {DropDownProps} disabled - Whether the dropdown is disabled - * @param {DropDownProps} size - Font size for title and icon dimensions in pixels - * @param {DropDownProps} styles - Custom Emotion CSS styles to applied to the button + * @param {DropDownProps} props - Dropdown configuration + * @param {string} title - Text displayed on the dropdown button + * @param {ReactNode} leftIcon - Custom icon displayed on the left side + * @param {boolean} disabled - Whether the dropdown is disabled + * @param {number} size - Font size for title and icon dimensions in pixels + * @param {SerializedStyles} styles - Custom Emotion CSS styles applied to the button + * @param {ReactNode} children - Content rendered inside the dropdown panel + * @param {boolean} closeOnSelect - Whether clicking inside the panel closes the dropdown * @returns {JSX.Element} Dropdown component */ - -const Dropdown = ({ menuItems = [], title, leftIcon, disabled = false, size = 16, styles, children, panelStyles }: DropDownProps) => { +const Dropdown = ({ + title, + leftIcon, + disabled = false, + size = 16, + styles, + children, + closeOnSelect = true, +}: DropDownProps) => { const [open, setOpen] = useState(false); const dropdownRef = useRef(null); const theme: Theme = useThemeContext(); @@ -159,14 +158,6 @@ const Dropdown = ({ menuItems = [], title, leftIcon, disabled = false, size = 16 [disabled], ); - const renderMenuItems = () => { - return menuItems.map(({ label, action }) => ( - setOpen(false)}> - {label} - - )); - }; - return (
{open && !disabled && ( - children ? -
{children}
- : - {renderMenuItems()} - +
setOpen(false) : undefined}> + {children} +
)}
); From bc26325af6f2ee70dc6fe4050ebe82557c11e19c Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 18 Mar 2026 12:46:14 -0400 Subject: [PATCH 13/43] refactor: change from multiple rows to individual rows --- .../Dropdown/FilterRow.tsx} | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) rename packages/ui/src/{viewer-table/Toolbar/FilterRows.tsx => common/Dropdown/FilterRow.tsx} (65%) diff --git a/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx b/packages/ui/src/common/Dropdown/FilterRow.tsx similarity index 65% rename from packages/ui/src/viewer-table/Toolbar/FilterRows.tsx rename to packages/ui/src/common/Dropdown/FilterRow.tsx index 92cd0687..5864202e 100644 --- a/packages/ui/src/viewer-table/Toolbar/FilterRows.tsx +++ b/packages/ui/src/common/Dropdown/FilterRow.tsx @@ -23,9 +23,13 @@ import { css } from '@emotion/react'; -import { useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; import { type Theme, useThemeContext } from '../../theme/index'; -import type { CustomFilterCategory } from '../DictionaryTableViewer'; + +export type FilterCategory = { + label: string; + filterProperty: string; + options: string[]; +}; const sectionHeaderStyle = (theme: Theme) => css` ${theme.typography.buttonText}; @@ -39,6 +43,7 @@ const sectionHeaderStyle = (theme: Theme) => css` `; const categoryGroupStyle = css` + min-width: 200px; &:first-of-type > div:first-of-type { border-top: none; } @@ -65,40 +70,30 @@ const checkboxLabelStyle = (theme: Theme) => css` user-select: none; `; -export type FilterRowsProps = { - categories: CustomFilterCategory[]; +export type FilterRowProps = { + category: FilterCategory; + showHeader: boolean; + selections: string[]; + onToggle: (option: string) => void; }; -const FilterRows = ({ categories }: FilterRowsProps) => { +const FilterRow = ({ category, showHeader, selections, onToggle }: FilterRowProps) => { const theme = useThemeContext(); - const { customFilterSelections, toggleCustomFilter } = useDictionaryStateContext(); - const hasMultipleCategories = categories.length > 1; return ( - <> - {categories.map((category) => { - const selected = customFilterSelections[category.filterProperty] ?? []; +
+ {showHeader &&
{category.label} Filter
} + {category.options.map((option) => { + const isChecked = selections.includes(option); return ( -
- {hasMultipleCategories &&
{category.label} Filter
} - {category.options.map((option) => { - const isChecked = selected.includes(option); - return ( - - ); - })} -
+ ); })} - +
); }; -export default FilterRows; +export default FilterRow; From 2e674762dd60276c33c68729cbbef332c6c613b1 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 18 Mar 2026 12:46:44 -0400 Subject: [PATCH 14/43] refactor: update index files --- packages/ui/src/common/Dropdown/index.ts | 1 + packages/ui/src/viewer-table/Toolbar/index.ts | 1 - packages/ui/src/viewer-table/index.ts | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/common/Dropdown/index.ts b/packages/ui/src/common/Dropdown/index.ts index 3004bdc0..d1e812b8 100644 --- a/packages/ui/src/common/Dropdown/index.ts +++ b/packages/ui/src/common/Dropdown/index.ts @@ -1 +1,2 @@ export { default, type DropDownProps } from './Dropdown'; +export { default as FilterRow, type FilterRowProps, type FilterCategory } from './FilterRow'; diff --git a/packages/ui/src/viewer-table/Toolbar/index.ts b/packages/ui/src/viewer-table/Toolbar/index.ts index b6dd9fe3..5c0f7f69 100644 --- a/packages/ui/src/viewer-table/Toolbar/index.ts +++ b/packages/ui/src/viewer-table/Toolbar/index.ts @@ -1,5 +1,4 @@ export { default as AttributeFilterButton } from './AttributeFilterButton.js'; -export { default as FilterRows, type FilterRowsProps } from './FilterRows.js'; export { default as CollapseAllButton, type CollapseAllButtonProps } from './CollapseAllButton.js'; export { default as DiagramViewButton } from './DiagramViewButton.js'; export { default as DictionaryDownloadButton, type DictionaryDownloadButtonProps } from './DictionaryDownloadButton.js'; diff --git a/packages/ui/src/viewer-table/index.ts b/packages/ui/src/viewer-table/index.ts index 9025fa79..dc103031 100644 --- a/packages/ui/src/viewer-table/index.ts +++ b/packages/ui/src/viewer-table/index.ts @@ -40,4 +40,5 @@ export { // TODO: study which of the following are worth exporting at root, if any export * from './DictionaryTableViewer'; +export { type FilterCategory } from '../common/Dropdown/index'; export * from './Loading'; From fb59de65cd805e8cdd06dd9824acbfcfbd239f67 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 18 Mar 2026 12:47:37 -0400 Subject: [PATCH 15/43] refactor: move mapping of components outside of dropdown component --- .../DictionaryVersionSwitcher.tsx | 17 +++++----- .../Toolbar/TableOfContentsDropdown.tsx | 14 ++++---- .../ui/src/viewer-table/Toolbar/Toolbar.tsx | 32 +++++++++---------- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/ui/src/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.tsx b/packages/ui/src/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.tsx index ae39a42f..f9672ab4 100644 --- a/packages/ui/src/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.tsx +++ b/packages/ui/src/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.tsx @@ -18,9 +18,8 @@ * */ -import { css } from '@emotion/react'; - import Dropdown from '../../common/Dropdown/index'; +import DropDownItem from '../../common/Dropdown/DropdownItem'; import { useDictionaryDataContext, useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; import History from '../../theme/icons/History'; @@ -55,13 +54,13 @@ const DictionaryVersionSwitcher = () => { return ( displayVersionSwitcher && ( - } - disabled={loading || errors.length > 0} - size={16} - /> + } disabled={loading || errors.length > 0} size={16}> + {versionSwitcherObjectArray.map(({ label, action }) => ( + + {label} + + ))} + ) ); }; diff --git a/packages/ui/src/viewer-table/Toolbar/TableOfContentsDropdown.tsx b/packages/ui/src/viewer-table/Toolbar/TableOfContentsDropdown.tsx index fea1e9bc..45951687 100644 --- a/packages/ui/src/viewer-table/Toolbar/TableOfContentsDropdown.tsx +++ b/packages/ui/src/viewer-table/Toolbar/TableOfContentsDropdown.tsx @@ -22,6 +22,7 @@ import type { Schema } from '@overture-stack/lectern-dictionary'; import Dropdown from '../../common/Dropdown/index'; +import DropDownItem from '../../common/Dropdown/DropdownItem'; import { useDictionaryDataContext } from '../../dictionary-controller/DictionaryDataContext'; import { type Theme, useThemeContext } from '../../theme/index'; @@ -41,12 +42,13 @@ const TableOfContentsDropdown = ({ schemas, onSelect }: TableOfContentsDropdownP })); return schemas.length > 0 ? - } - title="Table of Contents" - menuItems={menuItemsFromSchemas} - disabled={loading || errors.length > 0} - /> + } title="Table of Contents" disabled={loading || errors.length > 0}> + {menuItemsFromSchemas.map(({ label, action }) => ( + + {label} + + ))} + : null; }; diff --git a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx index 9dd9c9c4..717f5943 100644 --- a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx +++ b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx @@ -26,11 +26,8 @@ import { css } from '@emotion/react'; import { useDictionaryDataContext, useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; import { type Theme, useThemeContext } from '../../theme/index'; import { ToolbarSkeleton } from '../Loading'; -import type { CustomFilterCategory } from '../DictionaryTableViewer'; - -import Dropdown from '../../common/Dropdown/index'; +import Dropdown, { FilterRow, type FilterCategory } from '../../common/Dropdown/index'; import AttributeFilterButton from './AttributeFilterButton'; -import FilterRows from './FilterRows'; import CollapseAllButton from './CollapseAllButton'; import DiagramViewButton from './DiagramViewButton'; import DictionaryDownloadButton from './DictionaryDownloadButton'; @@ -41,7 +38,7 @@ export type ToolbarProps = { onSelect: (schemaNameIndex: number) => void; setIsCollapsed: (collapsed: boolean) => void; isCollapsed: boolean; - customFilterCategories?: CustomFilterCategory[]; + filterCategories?: FilterCategory[]; }; const panelStyles = (theme: Theme) => css` @@ -65,15 +62,10 @@ const sectionStyles = css` gap: 16px; `; -const customFilterPanelStyles = css` - min-width: 200px; - max-height: 300px; -`; - -const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, customFilterCategories }: ToolbarProps) => { +const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, filterCategories }: ToolbarProps) => { const theme: Theme = useThemeContext(); const { loading, errors } = useDictionaryDataContext(); - const { selectedDictionary } = useDictionaryStateContext(); + const { selectedDictionary, customFilterSelections, toggleCustomFilter } = useDictionaryStateContext(); const { ListFilter } = theme.icons; if (!selectedDictionary && !loading) { @@ -93,14 +85,22 @@ const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, customFilterCategories setIsCollapsed(false)} /> : setIsCollapsed(true)} />} - {customFilterCategories && customFilterCategories.length > 0 && ( + {filterCategories && filterCategories.length > 0 && ( } - title={customFilterCategories.length === 1 ? customFilterCategories[0].label : 'Filters'} + title={filterCategories.length === 1 ? filterCategories[0].label : 'Filters'} disabled={loading || errors.length > 0} - panelStyles={customFilterPanelStyles} + closeOnSelect={false} > - + {filterCategories.map((category) => ( + 1} + selections={customFilterSelections[category.filterProperty] ?? []} + onToggle={(option) => toggleCustomFilter(category.filterProperty, option)} + /> + ))} )}
From 410aad9bcbbed2804e4c8a49c9046e4772572ee1 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 18 Mar 2026 12:47:51 -0400 Subject: [PATCH 16/43] style: update storybook component --- .../ui/stories/common/DropDown.stories.tsx | 41 ++++++------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/packages/ui/stories/common/DropDown.stories.tsx b/packages/ui/stories/common/DropDown.stories.tsx index 98cd0026..8378738b 100644 --- a/packages/ui/stories/common/DropDown.stories.tsx +++ b/packages/ui/stories/common/DropDown.stories.tsx @@ -24,6 +24,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import Dropdown from '../../src/common/Dropdown/index'; +import DropDownItem from '../../src/common/Dropdown/DropdownItem'; import themeDecorator from '../themeDecorator'; @@ -41,20 +42,12 @@ export const Default: Story = { args: { title: 'Juice', leftIcon: '!', - menuItems: [ - { - label: 'Apple', - action: () => { - alert('apple juice!'); - }, - }, - { - label: 'Orange', - action: () => { - alert('orange juice :('); - }, - }, - ], + children: ( + <> + alert('apple juice!')}>Apple + alert('orange juice :(')}>Orange + + ), }, }; @@ -63,20 +56,12 @@ export const Disabled: Story = { title: 'Juice', leftIcon: '!', disabled: true, - menuItems: [ - { - label: 'Apple', - action: () => { - alert('apple juice!'); - }, - }, - { - label: 'Orange', - action: () => { - alert('orange juice :('); - }, - }, - ], + children: ( + <> + alert('apple juice!')}>Apple + alert('orange juice :(')}>Orange + + ), }, }; From cb3ac1d73c266090a906e68cae934e719eaad902 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 18 Mar 2026 12:52:46 -0400 Subject: [PATCH 17/43] refactor: rename from custom filter selection to filter selection --- .../DictionaryDataContext.tsx | 26 +++++++++---------- .../viewer-table/DictionaryTableViewer.tsx | 6 ++--- .../ui/src/viewer-table/Toolbar/Toolbar.tsx | 6 ++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx index 8868fa27..dd1183c5 100644 --- a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx +++ b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx @@ -37,7 +37,7 @@ export type DictionaryDataContextType = { errors: string[]; }; -export type CustomFilterSelections = Record; +export type FilterSelections = Record; export type DictionaryStateContextType = { currentDictionaryIndex: number; @@ -45,9 +45,9 @@ export type DictionaryStateContextType = { setCurrentDictionaryIndex: (index: number) => void; setFilters: (filters: FilterOptions[]) => void; selectedDictionary?: DictionaryServerUnion; - customFilterSelections: CustomFilterSelections; - toggleCustomFilter: (filterProperty: string, value: string) => void; - resetCustomFilters: () => void; + filterSelections: FilterSelections; + toggleFilter: (filterProperty: string, value: string) => void; + resetFilters: () => void; }; export type StaticDictionaryProviderProps = { @@ -181,26 +181,26 @@ export type DictionaryStateProviderProps = { export const DictionaryStateProvider = ({ children }: DictionaryStateProviderProps) => { const [currentDictionaryIndex, setCurrentDictionaryIndex] = useState(0); const [filters, setFilters] = useState([]); - const [customFilterSelections, setCustomFilterSelections] = useState({}); + const [filterSelections, setFilterSelections] = useState({}); const dictionaryData = useDictionaryDataContext(); const { dictionaries } = dictionaryData; const selectedDictionary = dictionaries?.[currentDictionaryIndex]; useEffect(() => { - setCustomFilterSelections({}); + setFilterSelections({}); }, [currentDictionaryIndex]); - const toggleCustomFilter = useCallback((filterProperty: string, value: string) => { - setCustomFilterSelections((prev) => { + const toggleFilter = useCallback((filterProperty: string, value: string) => { + setFilterSelections((prev) => { const current = prev[filterProperty] ?? []; const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]; return { ...prev, [filterProperty]: next }; }); }, []); - const resetCustomFilters = useCallback(() => { - setCustomFilterSelections({}); + const resetFilters = useCallback(() => { + setFilterSelections({}); }, []); const value: DictionaryStateContextType = { @@ -209,9 +209,9 @@ export const DictionaryStateProvider = ({ children }: DictionaryStateProviderPro setCurrentDictionaryIndex, setFilters, selectedDictionary, - customFilterSelections, - toggleCustomFilter, - resetCustomFilters, + filterSelections, + toggleFilter, + resetFilters, }; return {children}; diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index a552646a..9c12cb77 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -243,7 +243,7 @@ const DiagramModal = () => { const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTableViewerProps) => { const theme = useThemeContext(); const { loading, errors } = useDictionaryDataContext(); - const { filters, selectedDictionary, customFilterSelections, resetCustomFilters } = useDictionaryStateContext(); + const { filters, selectedDictionary, filterSelections, resetFilters } = useDictionaryStateContext(); const [isCollapsed, setIsCollapsed] = useState(false); const [selectedSchemaIndex, setSelectedSchemaIndex] = useState(undefined); @@ -325,7 +325,7 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable }, [handleHash]); const activeFilters: [string, string[]][] = (customFilterDropdowns ?? []).flatMap((dropdown) => { - const values = customFilterSelections[dropdown.filterProperty]; + const values = filterSelections[dropdown.filterProperty]; if (values === undefined || values.length === 0) { return []; @@ -411,7 +411,7 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable
No schemas match the selected filters
Try adjusting or clearing your filter selections
- diff --git a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx index 717f5943..828e5c4b 100644 --- a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx +++ b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx @@ -65,7 +65,7 @@ const sectionStyles = css` const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, filterCategories }: ToolbarProps) => { const theme: Theme = useThemeContext(); const { loading, errors } = useDictionaryDataContext(); - const { selectedDictionary, customFilterSelections, toggleCustomFilter } = useDictionaryStateContext(); + const { selectedDictionary, filterSelections, toggleFilter } = useDictionaryStateContext(); const { ListFilter } = theme.icons; if (!selectedDictionary && !loading) { @@ -97,8 +97,8 @@ const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, filterCategories }: To key={category.filterProperty} category={category} showHeader={filterCategories.length > 1} - selections={customFilterSelections[category.filterProperty] ?? []} - onToggle={(option) => toggleCustomFilter(category.filterProperty, option)} + selections={filterSelections[category.filterProperty] ?? []} + onToggle={(option) => toggleFilter(category.filterProperty, option)} /> ))}
From d4b801a1444a3bab416a858dfc637aaacd864c9f Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 19 Mar 2026 11:55:32 -0400 Subject: [PATCH 18/43] style: increase max height of dropdown component --- packages/ui/src/common/Dropdown/Dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/common/Dropdown/Dropdown.tsx b/packages/ui/src/common/Dropdown/Dropdown.tsx index c32fcc24..9ea89c5d 100644 --- a/packages/ui/src/common/Dropdown/Dropdown.tsx +++ b/packages/ui/src/common/Dropdown/Dropdown.tsx @@ -90,7 +90,7 @@ const dropdownMenuStyle = (theme: Theme) => css` border-radius: 9px; box-sizing: border-box; z-index: 100; - max-height: 150px; + max-height: 300px; overflow-y: auto; margin: 0; padding: 0; From 3891c9aa5910fdbf076dfbcdb6e56c7fb4b278bf Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 19 Mar 2026 11:55:41 -0400 Subject: [PATCH 19/43] style: decrease font size of filter row component --- packages/ui/src/common/Dropdown/FilterRow.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/common/Dropdown/FilterRow.tsx b/packages/ui/src/common/Dropdown/FilterRow.tsx index 5864202e..c0ebedd3 100644 --- a/packages/ui/src/common/Dropdown/FilterRow.tsx +++ b/packages/ui/src/common/Dropdown/FilterRow.tsx @@ -33,9 +33,9 @@ export type FilterCategory = { const sectionHeaderStyle = (theme: Theme) => css` ${theme.typography.buttonText}; - font-size: 13px; + font-size: 10px; color: ${theme.colors.accent_dark}; - padding: 6px 12px; + padding: 6px 8px; border-top: 1px solid ${theme.colors.border_button}; border-bottom: 1px solid ${theme.colors.border_button}; text-transform: uppercase; @@ -43,7 +43,6 @@ const sectionHeaderStyle = (theme: Theme) => css` `; const categoryGroupStyle = css` - min-width: 200px; &:first-of-type > div:first-of-type { border-top: none; } @@ -53,7 +52,7 @@ const checkboxRowStyle = (theme: Theme) => css` display: flex; align-items: center; gap: 8px; - padding: 4px 12px; + padding: 4px; cursor: pointer; &:hover { background-color: ${theme.colors.accent_1}; @@ -65,6 +64,7 @@ const checkboxRowStyle = (theme: Theme) => css` const checkboxLabelStyle = (theme: Theme) => css` ${theme.typography.body}; + font-size: 13px; color: ${theme.colors.accent_dark}; cursor: pointer; user-select: none; From 11dd3be1f20f9518d98b804082a3a4f351be1cae Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 19 Mar 2026 11:56:44 -0400 Subject: [PATCH 20/43] refactor: add active filters as a type --- .../src/dictionary-controller/DictionaryDataContext.tsx | 2 ++ packages/ui/src/viewer-table/DictionaryTableViewer.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx index dd1183c5..ed625b24 100644 --- a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx +++ b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx @@ -39,6 +39,8 @@ export type DictionaryDataContextType = { export type FilterSelections = Record; +export type ActiveFilter = [string, string[]]; + export type DictionaryStateContextType = { currentDictionaryIndex: number; filters: FilterOptions[]; diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index 9c12cb77..eb658851 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -29,7 +29,7 @@ import Accordion from '../common/Accordion/index'; import type { FilterCategory } from '../common/Dropdown/index'; import Modal from '../common/Modal'; import { ErrorModal } from '../common/Error/ErrorModal'; -import { useDictionaryDataContext, useDictionaryStateContext } from '../dictionary-controller/DictionaryDataContext'; +import { type ActiveFilter, useDictionaryDataContext, useDictionaryStateContext } from '../dictionary-controller/DictionaryDataContext'; import { type Theme, useThemeContext } from '../theme/index'; import { isFieldRequired } from '../utils/isFieldRequired'; import { DiagramViewProvider, useDiagramViewContext } from './DiagramViewContext'; @@ -157,8 +157,8 @@ const isConditionalRestriction = (schemaFieldRestriction: SchemaFieldRestriction return schemaFieldRestriction && 'if' in schemaFieldRestriction && schemaFieldRestriction.if !== undefined; }; -const getFilteredSchema = (schema: Schema, filters: string[], activeFilters: [string, string[]][]): Schema | null => { - // Schema-level: hide entire schema if it doesn't match active custom filters +const getFilteredSchema = (schema: Schema, filters: string[], activeFilters: ActiveFilter[]): Schema | null => { + // Schema-level: hide entire schema if it doesn't match active filters // Within a category: OR (schema matches any selected value) // Across categories: AND (schema must match all categories) if (activeFilters.length > 0) { @@ -324,7 +324,7 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable return () => window.removeEventListener('hashchange', handleHash); }, [handleHash]); - const activeFilters: [string, string[]][] = (customFilterDropdowns ?? []).flatMap((dropdown) => { + const activeFilters: ActiveFilter[] = (filterDropdowns ?? []).flatMap((dropdown) => { const values = filterSelections[dropdown.filterProperty]; if (values === undefined || values.length === 0) { From 014f63c53650c4c1149f860cd22b647ac6b1dc2c Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 19 Mar 2026 11:57:23 -0400 Subject: [PATCH 21/43] refactor: rename custom filter dropdown to filter dropdown --- .../src/viewer-table/DictionaryTableViewer.tsx | 16 ++++++++-------- .../ui/src/viewer-table/DictionaryViewerPage.tsx | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index eb658851..ddff0ce9 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -47,13 +47,13 @@ import DictionaryViewerLoadingPage from './DictionaryViewer/DictionaryViewerLoad import DiagramSubtitle from './Toolbar/DiagramSubtitle'; import Toolbar from './Toolbar/index'; -export type CustomFilterDropdown = { +export type FilterDropdown = { label: string; filterProperty: string; }; export type DictionaryTableViewerProps = { - customFilterDropdowns?: CustomFilterDropdown[]; + filterDropdowns?: FilterDropdown[]; }; const getByDotPath = (obj: unknown, path: string): unknown => @@ -240,7 +240,7 @@ const DiagramModal = () => { // TODO: produce a simplified version that accepts a dictionary and produces this same view, // so that there's no requirement for a Lectern server, etc. and without a Toolbar, or a simpler one. -const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTableViewerProps) => { +const DictionaryTableViewerContent = ({ filterDropdowns }: DictionaryTableViewerProps) => { const theme = useThemeContext(); const { loading, errors } = useDictionaryDataContext(); const { filters, selectedDictionary, filterSelections, resetFilters } = useDictionaryStateContext(); @@ -256,11 +256,11 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable ); const filterCategories: FilterCategory[] | undefined = useMemo(() => { - if (!customFilterDropdowns?.length || !selectedDictionary?.schemas) { + if (!filterDropdowns?.length || !selectedDictionary?.schemas) { return undefined; } - const dropdownContexts = customFilterDropdowns.map((dropdown) => ({ + const dropdownContexts = filterDropdowns.map((dropdown) => ({ dropdown, set: new Set(), })); @@ -286,7 +286,7 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable filterProperty: dropdown.filterProperty, options: Array.from(set), })); - }, [customFilterDropdowns, selectedDictionary?.schemas]); + }, [filterDropdowns, selectedDictionary?.schemas]); const handleHash = useCallback(() => { const target = parseHash(window.location.hash, selectedDictionary?.schemas); @@ -427,9 +427,9 @@ const DictionaryTableViewerContent = ({ customFilterDropdowns }: DictionaryTable ); }; -export const DictionaryTableViewer = ({ customFilterDropdowns }: DictionaryTableViewerProps) => ( +export const DictionaryTableViewer = ({ filterDropdowns }: DictionaryTableViewerProps) => ( - + ); diff --git a/packages/ui/src/viewer-table/DictionaryViewerPage.tsx b/packages/ui/src/viewer-table/DictionaryViewerPage.tsx index ef1c405c..7134af3a 100644 --- a/packages/ui/src/viewer-table/DictionaryViewerPage.tsx +++ b/packages/ui/src/viewer-table/DictionaryViewerPage.tsx @@ -21,23 +21,23 @@ import { DictionaryLecternDataProvider, DictionaryStateProvider } from '../dictionary-controller/DictionaryDataContext'; import DictionaryTableViewer from './DictionaryTableViewer'; -import type { CustomFilterDropdown } from './DictionaryTableViewer'; +import type { FilterDropdown } from './DictionaryTableViewer'; export type DictionaryTableProps = { lecternUrl: string; dictionaryName: string; - customFilterDropdowns?: CustomFilterDropdown[]; + filterDropdowns?: FilterDropdown[]; }; /** * DictionaryViewerPage component. * @param {DictionaryTableProps} props */ -const DictionaryViewerPage = ({ lecternUrl, dictionaryName, customFilterDropdowns }: DictionaryTableProps) => { +const DictionaryViewerPage = ({ lecternUrl, dictionaryName, filterDropdowns }: DictionaryTableProps) => { return ( - + ); From d628a1ffe56216dbe0d2f50b8cdb0b34e39122cc Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 19 Mar 2026 11:58:14 -0400 Subject: [PATCH 22/43] refactor: update stories naming from custom filter dropdowns to filter dropdowns --- packages/ui/stories/dictionaryDecorator.tsx | 4 ++-- .../viewer-table/DictionaryViewerPage.stories.tsx | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ui/stories/dictionaryDecorator.tsx b/packages/ui/stories/dictionaryDecorator.tsx index 296ce368..021c7760 100644 --- a/packages/ui/stories/dictionaryDecorator.tsx +++ b/packages/ui/stories/dictionaryDecorator.tsx @@ -145,7 +145,7 @@ const schemaMetaMap: Record = { read_group: { submitter: 'Researcher', domain: 'Clinical' }, }; -const customFilterDictionaryData: DictionaryTestData = [ +const filterDictionaryData: DictionaryTestData = [ { ...DictionarySample, schemas: DictionarySample.schemas.map((schema) => ({ @@ -158,4 +158,4 @@ const customFilterDictionaryData: DictionaryTestData = [ export const withMultipleDictionaries = withDictionaryContext(multipleDictionaryData); export const withSingleDictionary = withDictionaryContext(singleDictionaryData); export const withEmptyDictionaries = withDictionaryContext(emptyDictionaryData); -export const withCustomFilterDictionary = withDictionaryContext(customFilterDictionaryData); +export const withFilterDictionary = withDictionaryContext(filterDictionaryData); diff --git a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx index 56ab390c..fa8aed64 100644 --- a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx +++ b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx @@ -32,7 +32,7 @@ import { withLecternUrl, withMultipleDictionaries, withSingleDictionary, - withCustomFilterDictionary, + withFilterDictionary, } from '../dictionaryDecorator'; import themeDecorator from '../themeDecorator'; @@ -95,17 +95,17 @@ export const LecternServer: Story = { decorators: [withLecternUrl()], }; -export const WithSingleCustomFilter: Story = { - decorators: [withCustomFilterDictionary], +export const WithSingleFilter: Story = { + decorators: [withFilterDictionary], args: { - customFilterDropdowns: [{ label: 'Submitter', filterProperty: 'meta.submitter' }], + filterDropdowns: [{ label: 'Submitter', filterProperty: 'meta.submitter' }], }, }; -export const WithCustomFilterDropdowns: Story = { - decorators: [withCustomFilterDictionary], +export const WithFilterDropdowns: Story = { + decorators: [withFilterDictionary], args: { - customFilterDropdowns: [ + filterDropdowns: [ { label: 'Submitter', filterProperty: 'meta.submitter' }, { label: 'Domain', filterProperty: 'meta.domain' }, ], From a0813768c9f443560e1d663432a65a59c8dc76fe Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 19 Mar 2026 11:58:35 -0400 Subject: [PATCH 23/43] fix: resolve naming issue --- ...n.stories.tsx => AttributeFilterButton.stories.tsx} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename packages/ui/stories/viewer-table/Toolbar/{AttributeFilterDropdown.stories.tsx => AttributeFilterButton.stories.tsx} (83%) diff --git a/packages/ui/stories/viewer-table/Toolbar/AttributeFilterDropdown.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/AttributeFilterButton.stories.tsx similarity index 83% rename from packages/ui/stories/viewer-table/Toolbar/AttributeFilterDropdown.stories.tsx rename to packages/ui/stories/viewer-table/Toolbar/AttributeFilterButton.stories.tsx index 7e8867d0..f9889e96 100644 --- a/packages/ui/stories/viewer-table/Toolbar/AttributeFilterDropdown.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/AttributeFilterButton.stories.tsx @@ -22,25 +22,25 @@ import type { Meta, StoryObj } from '@storybook/react'; -import AttributeFilter from '../../../src/viewer-table/Toolbar/AttributeFilterDropdown'; +import AttributeFilterButton from '../../../src/viewer-table/Toolbar/AttributeFilterButton'; import { withLoadingState, withMultipleDictionaries } from '../../dictionaryDecorator'; import themeDecorator from '../../themeDecorator'; const meta = { - component: AttributeFilter, - title: 'Viewer - Table/Toolbar/AttributeFilterDropdown', + component: AttributeFilterButton, + title: 'Viewer - Table/Toolbar/AttributeFilterButton', tags: ['autodocs'], decorators: [themeDecorator(), withMultipleDictionaries], parameters: { docs: { description: { component: - 'A dropdown component that allows users to filter dictionary fields by attributes such as required fields or all fields.', + 'A button component that allows users to filter dictionary fields by attributes such as required fields or all fields.', }, }, }, -} satisfies Meta; +} satisfies Meta; export default meta; From f392cd84c976c57f0891f0e5e9d8e213216d90e4 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 19 Mar 2026 12:06:05 -0400 Subject: [PATCH 24/43] fix: correct active filter type --- .../ui/src/dictionary-controller/DictionaryDataContext.tsx | 2 +- packages/ui/src/viewer-table/DictionaryTableViewer.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx index ed625b24..486a6817 100644 --- a/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx +++ b/packages/ui/src/dictionary-controller/DictionaryDataContext.tsx @@ -39,7 +39,7 @@ export type DictionaryDataContextType = { export type FilterSelections = Record; -export type ActiveFilter = [string, string[]]; +export type ActiveFilter = [string, string[]][]; export type DictionaryStateContextType = { currentDictionaryIndex: number; diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index ddff0ce9..60960add 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -157,7 +157,7 @@ const isConditionalRestriction = (schemaFieldRestriction: SchemaFieldRestriction return schemaFieldRestriction && 'if' in schemaFieldRestriction && schemaFieldRestriction.if !== undefined; }; -const getFilteredSchema = (schema: Schema, filters: string[], activeFilters: ActiveFilter[]): Schema | null => { +const getFilteredSchema = (schema: Schema, filters: string[], activeFilters: ActiveFilter): Schema | null => { // Schema-level: hide entire schema if it doesn't match active filters // Within a category: OR (schema matches any selected value) // Across categories: AND (schema must match all categories) @@ -324,7 +324,7 @@ const DictionaryTableViewerContent = ({ filterDropdowns }: DictionaryTableViewer return () => window.removeEventListener('hashchange', handleHash); }, [handleHash]); - const activeFilters: ActiveFilter[] = (filterDropdowns ?? []).flatMap((dropdown) => { + const activeFilters: ActiveFilter = (filterDropdowns ?? []).flatMap((dropdown) => { const values = filterSelections[dropdown.filterProperty]; if (values === undefined || values.length === 0) { From 3066bad93ecfecedd80e70f570a0b0f7101a7b7b Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Mon, 23 Mar 2026 23:15:25 -0400 Subject: [PATCH 25/43] feat: add optional tag prop and conditional rendering --- .../ui/src/common/Accordion/Accordion.tsx | 7 ++++ .../ui/src/common/Accordion/AccordionItem.tsx | 34 +++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/common/Accordion/Accordion.tsx b/packages/ui/src/common/Accordion/Accordion.tsx index 9fe8ed1e..ca959d8d 100644 --- a/packages/ui/src/common/Accordion/Accordion.tsx +++ b/packages/ui/src/common/Accordion/Accordion.tsx @@ -26,12 +26,19 @@ import { type ReactNode, useEffect, useState } from 'react'; import AccordionItem from './AccordionItem'; +export type TagPill = { + label: string; + value: string; +}; + export type AccordionData = { title: string; description?: string; content: ReactNode; schemaName?: string; + tags?: TagPill[]; }; + export type AccordionProps = { accordionItems: Array; collapseAll: boolean; diff --git a/packages/ui/src/common/Accordion/AccordionItem.tsx b/packages/ui/src/common/Accordion/AccordionItem.tsx index d742de76..d3845887 100644 --- a/packages/ui/src/common/Accordion/AccordionItem.tsx +++ b/packages/ui/src/common/Accordion/AccordionItem.tsx @@ -25,6 +25,7 @@ import { css } from '@emotion/react'; import { MouseEvent, useRef } from 'react'; import { useClipboard } from '../../hooks/useClipboard'; +import { pillStyle } from '../../theme/emotion/index'; import { type Theme, useThemeContext } from '../../theme/index'; import DictionaryDownloadButton from '../../viewer-table/Toolbar/DictionaryDownloadButton'; import ReadMoreText from '../ReadMoreText'; @@ -151,6 +152,22 @@ const downloadButtonContainerStyle = css` margin-left: auto; `; +const tagPillContainerStyle = css` + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + margin-top: 6px; +`; + +const tagLabelStyle = (theme: Theme) => css` + display: inline-flex; + align-items: center; + gap: 4px; + ${theme.typography.data}; + color: ${theme.colors.black}; +`; + const hashOnClick = ( event: MouseEvent, windowLocationHash: string, @@ -169,8 +186,8 @@ const AccordionItem = ({ index, accordionData, openState }: AccordionItemProps) const theme: Theme = useThemeContext(); const { setClipboardContents } = useClipboard(); - const { description, title, content, schemaName } = accordionData; - const { ChevronDown, Hash } = theme.icons; + const { description, title, content, schemaName, tags } = accordionData; + const { ChevronDown, Hash, Tags } = theme.icons; const anchorId = schemaName || index.toString(); const windowLocationHash = `#${anchorId}`; @@ -207,6 +224,19 @@ const AccordionItem = ({ index, accordionData, openState }: AccordionItemProps) {description}
+ {tags && tags.length > 0 && ( +
+ + + Tags + + {tags.map((pill, index) => ( + + {pill.label}: {pill.value} + + ))} +
+ )} {schemaName && (
e.stopPropagation()}> From 3fb6154b63cf7685d7444425aeebd1b5bcf4c271 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Mon, 23 Mar 2026 23:16:41 -0400 Subject: [PATCH 26/43] style: add pillstyle to common --- packages/ui/src/theme/emotion/common.ts | 14 ++++++++++++++ packages/ui/src/theme/emotion/index.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/theme/emotion/common.ts b/packages/ui/src/theme/emotion/common.ts index 4d039ada..b0b259c7 100644 --- a/packages/ui/src/theme/emotion/common.ts +++ b/packages/ui/src/theme/emotion/common.ts @@ -20,7 +20,21 @@ */ import { css } from '@emotion/react'; +import type { Theme } from '../index'; export const NoMarginParagraph = css` margin: 0; +`; + +export const pillStyle = (theme: Theme) => css` + display: inline-flex; + align-items: center; + font-size: 16px; + padding: 2px 8px; + border: 0.5px solid ${theme.colors.accent_dark}; + border-radius: 20px; + background-color: rgba(255, 255, 255, 0.15); + color: ${theme.colors.accent_dark}; + ${theme.typography.data}; + gap: 3px; `; \ No newline at end of file diff --git a/packages/ui/src/theme/emotion/index.ts b/packages/ui/src/theme/emotion/index.ts index 40531ef8..8e1a5b4f 100644 --- a/packages/ui/src/theme/emotion/index.ts +++ b/packages/ui/src/theme/emotion/index.ts @@ -19,4 +19,4 @@ * */ -export { NoMarginParagraph } from './common'; +export { NoMarginParagraph, pillStyle } from './common'; From d4147ac33a36dc701d5335524857d372a6f8cf93 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Mon, 23 Mar 2026 23:16:50 -0400 Subject: [PATCH 27/43] style: add new tags icon --- packages/ui/src/theme/icons/Tags.tsx | 51 +++++++++++++++++++++++++++ packages/ui/src/theme/styles/icons.ts | 2 ++ 2 files changed, 53 insertions(+) create mode 100644 packages/ui/src/theme/icons/Tags.tsx diff --git a/packages/ui/src/theme/icons/Tags.tsx b/packages/ui/src/theme/icons/Tags.tsx new file mode 100644 index 00000000..39bfe010 --- /dev/null +++ b/packages/ui/src/theme/icons/Tags.tsx @@ -0,0 +1,51 @@ +/* + * + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/** @jsxImportSource @emotion/react */ + +import IconProps from './IconProps'; + +const Tags = ({ fill, width, height, style }: IconProps) => { + return ( + + + + + ); +}; + +export default Tags; diff --git a/packages/ui/src/theme/styles/icons.ts b/packages/ui/src/theme/styles/icons.ts index ed10953e..ebac41fd 100644 --- a/packages/ui/src/theme/styles/icons.ts +++ b/packages/ui/src/theme/styles/icons.ts @@ -33,6 +33,7 @@ import Minus from '../icons/Minus'; import Plus from '../icons/Plus'; import Refresh from '../icons/Refresh'; import Spinner from '../icons/Spinner'; +import Tags from '../icons/Tags'; export default { ChevronDown, @@ -50,4 +51,5 @@ export default { Plus, Minus, Refresh, + Tags, }; From eca3ebfaf88a9e75ac9b5cddfc74d05f5d33c564 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Mon, 23 Mar 2026 23:17:21 -0400 Subject: [PATCH 28/43] feat: render pills within accordions --- .../viewer-table/DictionaryTableViewer.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index 60960add..2779b3a8 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -29,7 +29,11 @@ import Accordion from '../common/Accordion/index'; import type { FilterCategory } from '../common/Dropdown/index'; import Modal from '../common/Modal'; import { ErrorModal } from '../common/Error/ErrorModal'; -import { type ActiveFilter, useDictionaryDataContext, useDictionaryStateContext } from '../dictionary-controller/DictionaryDataContext'; +import { + type ActiveFilter, + useDictionaryDataContext, + useDictionaryStateContext, +} from '../dictionary-controller/DictionaryDataContext'; import { type Theme, useThemeContext } from '../theme/index'; import { isFieldRequired } from '../utils/isFieldRequired'; import { DiagramViewProvider, useDiagramViewContext } from './DiagramViewContext'; @@ -335,6 +339,8 @@ const DictionaryTableViewerContent = ({ filterDropdowns }: DictionaryTableViewer }); const accordionItems = useMemo(() => { + const dropdownMap = new Map((filterDropdowns ?? []).map((dropdown) => [dropdown.filterProperty, dropdown.label])); + return (selectedDictionary?.schemas ?? []).flatMap((schema: Schema) => { const filtered = getFilteredSchema(schema, filters, activeFilters); @@ -342,6 +348,23 @@ const DictionaryTableViewerContent = ({ filterDropdowns }: DictionaryTableViewer return []; } + const pills = activeFilters.flatMap(([filterProperty, selectedValues]) => { + const label = dropdownMap.get(filterProperty); + + if (!label) { + return []; + } + + const schemaVal = getByDotPath(schema, filterProperty); + + if (schemaVal == null) { + return []; + } + + const schemaValuesSet = new Set(Array.isArray(schemaVal) ? schemaVal.map(String) : [String(schemaVal)]); + return selectedValues.filter((value) => schemaValuesSet.has(value)).map((value) => ({ label, value })); + }); + return [ { title: schema.name, @@ -353,6 +376,7 @@ const DictionaryTableViewerContent = ({ filterDropdowns }: DictionaryTableViewer /> ), schemaName: schema.name, + tags: pills, }, ]; }); From 784fa3633f7f97616bd7ee9898bd53dce484210d Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Mon, 23 Mar 2026 23:17:28 -0400 Subject: [PATCH 29/43] feat: add active filter bar component --- .../viewer-table/Toolbar/ActiveFilterBar.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 packages/ui/src/viewer-table/Toolbar/ActiveFilterBar.tsx diff --git a/packages/ui/src/viewer-table/Toolbar/ActiveFilterBar.tsx b/packages/ui/src/viewer-table/Toolbar/ActiveFilterBar.tsx new file mode 100644 index 00000000..3e07ecf8 --- /dev/null +++ b/packages/ui/src/viewer-table/Toolbar/ActiveFilterBar.tsx @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** @jsxImportSource @emotion/react */ + +import { css } from '@emotion/react'; +import { useDictionaryStateContext } from '../../dictionary-controller/DictionaryDataContext'; +import { pillStyle } from '../../theme/emotion/index'; +import { type Theme, useThemeContext } from '../../theme/index'; + +const barStyles = (theme: Theme) => css` + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 12px; + padding: 12px 0px; + background-color: ${theme.colors.accent_1}; + border-block: 1px solid ${theme.colors.border_light}; +`; + +const labelStyles = (theme: Theme) => css` + ${theme.typography.subtitleBold}; + font-size: 14px; + color: ${theme.colors.accent}; + text-transform: uppercase; +`; + +const separatorStyles = (theme: Theme) => css` + width: 1px; + height: 20px; + background-color: ${theme.colors.accent_dark}; + flex-shrink: 0; + margin: 0 4px; +`; + +const pillCloseStyles = (theme: Theme) => css` + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: none; + background: none; + padding: 0; + color: ${theme.colors.accent}; + line-height: 1; + + &:hover { + color: ${theme.colors.accent_dark}; + } +`; + +const andStyles = (theme: Theme) => css` + ${theme.typography.dataBold}; + color: ${theme.colors.accent}; + text-transform: uppercase; +`; + +const filterEntryStyles = css` + display: flex; + align-items: center; + gap: 8px; +`; + +const resetStyles = (theme: Theme) => css` + ${theme.typography.data}; + color: ${theme.colors.accent}; + text-decoration: underline; + cursor: pointer; + background: none; + border: none; + padding: 0; + margin-left: 4px; + white-space: nowrap; +`; + +const ActiveFilterBar = () => { + const theme = useThemeContext(); + const { filterSelections, toggleFilter, resetFilters } = useDictionaryStateContext(); + const { Cancel } = theme.icons; + + const formatCategory = (category: string) => { + const label = category.split('.')[1] ?? category; + return label.charAt(0).toUpperCase() + label.slice(1); + }; + + const entries = Object.entries(filterSelections).flatMap(([category, values]) => + values.map((value) => ({ category, value })), + ); + + if (entries.length === 0) { + return null; + } + + return ( +
+ Active Filters +
+ {entries.map((entry, index) => ( +
+ {index > 0 && AND} + + {formatCategory(entry.category)}: {entry.value} + + +
+ ))} + +
+ ); +}; + +export default ActiveFilterBar; From 3a82bcd63bfbe69c1325b455f150f7db3e60242c Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Mon, 23 Mar 2026 23:17:38 -0400 Subject: [PATCH 30/43] feat: add active filter bar to toolbar --- .../ui/src/viewer-table/Toolbar/Toolbar.tsx | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx index 828e5c4b..45c2cd47 100644 --- a/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx +++ b/packages/ui/src/viewer-table/Toolbar/Toolbar.tsx @@ -33,6 +33,7 @@ import DiagramViewButton from './DiagramViewButton'; import DictionaryDownloadButton from './DictionaryDownloadButton'; import ExpandAllButton from './ExpandAllButton'; import TableOfContentsDropdown from './TableOfContentsDropdown'; +import ActiveFilterBar from './ActiveFilterBar'; export type ToolbarProps = { onSelect: (schemaNameIndex: number) => void; @@ -43,10 +44,8 @@ export type ToolbarProps = { const panelStyles = (theme: Theme) => css` display: flex; - width: 100% - width: -webkit-fit-content; - align-items: center; - justify-content: space-between; + flex-direction: column; + width: 100%; padding: 16px 0; background-color: ${theme.colors.white}; flex-wrap: nowrap; @@ -55,6 +54,13 @@ const panelStyles = (theme: Theme) => css` top: 0px; `; +const buttonRowStyles = css` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +`; + const sectionStyles = css` display: flex; align-items: center; @@ -78,35 +84,38 @@ const Toolbar = ({ onSelect, setIsCollapsed, isCollapsed, filterCategories }: To return (
-
- - - {isCollapsed ? - setIsCollapsed(false)} /> - : setIsCollapsed(true)} />} - - {filterCategories && filterCategories.length > 0 && ( - } - title={filterCategories.length === 1 ? filterCategories[0].label : 'Filters'} - disabled={loading || errors.length > 0} - closeOnSelect={false} - > - {filterCategories.map((category) => ( - 1} - selections={filterSelections[category.filterProperty] ?? []} - onToggle={(option) => toggleFilter(category.filterProperty, option)} - /> - ))} - - )} -
-
- +
+
+ + + {isCollapsed ? + setIsCollapsed(false)} /> + : setIsCollapsed(true)} />} + + {filterCategories && filterCategories.length > 0 && ( + } + title={filterCategories.length === 1 ? filterCategories[0].label : 'Filters'} + disabled={loading || errors.length > 0} + closeOnSelect={false} + > + {filterCategories.map((category) => ( + 1} + selections={filterSelections[category.filterProperty] ?? []} + onToggle={(option) => toggleFilter(category.filterProperty, option)} + /> + ))} + + )} +
+
+ +
+
); }; From 6cfcc21d0def64517fef57cbe7032857007db2f5 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Tue, 24 Mar 2026 09:58:08 -0400 Subject: [PATCH 31/43] refactor: change tags to be independant of active filter selections --- .../viewer-table/DictionaryTableViewer.tsx | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx index 2779b3a8..a5b8db77 100644 --- a/packages/ui/src/viewer-table/DictionaryTableViewer.tsx +++ b/packages/ui/src/viewer-table/DictionaryTableViewer.tsx @@ -339,8 +339,6 @@ const DictionaryTableViewerContent = ({ filterDropdowns }: DictionaryTableViewer }); const accordionItems = useMemo(() => { - const dropdownMap = new Map((filterDropdowns ?? []).map((dropdown) => [dropdown.filterProperty, dropdown.label])); - return (selectedDictionary?.schemas ?? []).flatMap((schema: Schema) => { const filtered = getFilteredSchema(schema, filters, activeFilters); @@ -348,21 +346,15 @@ const DictionaryTableViewerContent = ({ filterDropdowns }: DictionaryTableViewer return []; } - const pills = activeFilters.flatMap(([filterProperty, selectedValues]) => { - const label = dropdownMap.get(filterProperty); - - if (!label) { - return []; - } - - const schemaVal = getByDotPath(schema, filterProperty); + const pills = (filterDropdowns ?? []).flatMap((dropdown) => { + const value = getByDotPath(schema, dropdown.filterProperty); - if (schemaVal == null) { + if (value == null) { return []; } - - const schemaValuesSet = new Set(Array.isArray(schemaVal) ? schemaVal.map(String) : [String(schemaVal)]); - return selectedValues.filter((value) => schemaValuesSet.has(value)).map((value) => ({ label, value })); + + const values = Array.isArray(value) ? value.map(String) : [String(value)]; + return values.map((value) => ({ label: dropdown.label, value })); }); return [ From 25ac1724cafc45d453a7ce8cb5e78ff326f882d0 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 16:38:42 -0400 Subject: [PATCH 32/43] refactor: remove legacy schema diagram stories --- .../SchemaDiagramNode.stories.tsx | 49 --------- .../SchemaRelationshipDiagram.stories.tsx | 104 ------------------ 2 files changed, 153 deletions(-) delete mode 100644 packages/ui/stories/viewer-diagram/SchemaDiagramNode.stories.tsx delete mode 100644 packages/ui/stories/viewer-diagram/SchemaRelationshipDiagram.stories.tsx diff --git a/packages/ui/stories/viewer-diagram/SchemaDiagramNode.stories.tsx b/packages/ui/stories/viewer-diagram/SchemaDiagramNode.stories.tsx deleted file mode 100644 index 1c8256fe..00000000 --- a/packages/ui/stories/viewer-diagram/SchemaDiagramNode.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved - * - * This program and the accompanying materials are made available under the terms of - * the GNU Affero General Public License v3.0. You should have received a copy of the - * GNU Affero General Public License along with this program. - * If not, see . - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ -/** @jsxImportSource @emotion/react */ - -import type { Meta, StoryObj } from '@storybook/react'; -import { ReactFlowProvider } from 'reactflow'; - -import { SchemaDiagramNode } from '../../src/viewer-diagram/SchemaDiagramNode'; -import { buildSchemaNode } from '../../src/viewer-diagram/SchemaFlowNode'; - -import websiteUserDictionary from '../fixtures/websiteUsersDataDictionary'; - -const meta = { - component: SchemaDiagramNode, - title: 'Viewer - Diagram/Schema Diagram Node', - tags: ['autodocs'], - - render: (args) => ( - - - - ), -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const BiosampleDonor: Story = { - // args: { name: sampleDictionary.name, version: sampleDictionary.name, description: sampleDictionary.description }, - args: { data: buildSchemaNode(websiteUserDictionary.schemas[0]).data }, -}; diff --git a/packages/ui/stories/viewer-diagram/SchemaRelationshipDiagram.stories.tsx b/packages/ui/stories/viewer-diagram/SchemaRelationshipDiagram.stories.tsx deleted file mode 100644 index b042fb5d..00000000 --- a/packages/ui/stories/viewer-diagram/SchemaRelationshipDiagram.stories.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * - * Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved - * - * This program and the accompanying materials are made available under the terms of - * the GNU Affero General Public License v3.0. You should have received a copy of the - * GNU Affero General Public License along with this program. - * If not, see . - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ -/** @jsxImportSource @emotion/react */ - -import type { Schema, Values } from '@overture-stack/lectern-dictionary'; -import type { Meta, StoryObj } from '@storybook/react'; - -import { SchemaRelationshipDiagram } from '../../src/viewer-diagram/SchemaRelationshipDiagram'; - -import biosampleDictionary from '../fixtures/minimalBiosampleModel'; -import websiteUserDictionary from '../fixtures/websiteUsersDataDictionary'; - -const meta = { - component: SchemaRelationshipDiagram, - title: 'Viewer - Diagram/Schema Relationship Diagram', - tags: ['autodocs'], - - render: (args) => ( -
- -
- ), -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const BiosampleDictionary: Story = { - // args: { name: sampleDictionary.name, version: sampleDictionary.name, description: sampleDictionary.description }, - args: { dictionary: biosampleDictionary, layout: { columnWidth: 600, maxColumns: 4, rowHeight: 500 } }, -}; -export const TwoColumns: Story = { - // args: { name: sampleDictionary.name, version: sampleDictionary.name, description: sampleDictionary.description }, - args: { dictionary: biosampleDictionary, layout: { columnWidth: 600, maxColumns: 2, rowHeight: 500 } }, -}; - -export const WebsiteUserDictionary: Story = { - args: { dictionary: websiteUserDictionary }, -}; - -type X = { a: string }; - -export const SeparatedValueFileTypes = { - tsv: 'tsv', - csv: 'csv', -} as const; -export type SeparatedValueFileType = Values; - -export type DataFileTemplateConfig = { delimiter: string; extension: string }; - -const SeparatedValueFileConfigs = { - csv: { delimiter: ',', extension: 'csv' }, - tsv: { delimiter: '\t', extension: 'tsv' }, -} as const satisfies Record; - -export type CreateDataFileTemplateOptions = { fileType: SeparatedValueFileType } | DataFileTemplateConfig; -export type DataFileTemplate = { fileName: string; content: string }; - -export function createDataFileTemplate(schema: Schema, options?: CreateDataFileTemplateOptions): DataFileTemplate { - const config = - !options ? SeparatedValueFileConfigs.tsv - : 'fileType' in options ? SeparatedValueFileConfigs[options.fileType] - : options; - - // Build header row from field names - const header = schema.fields.map((field) => field.name); - - // Build a single empty data row (just placeholders) - const dataRow = schema.fields.map(() => ''); - - // Join header and row into TSV string - const lines = [header.join(config.delimiter), dataRow.join(config.delimiter)]; - - const content = lines.join('\n') + '\n'; - - return { - fileName: `${schema.name}.${config.extension}`, - content, - }; -} - -const x: CreateDataFileTemplateOptions = { fileType: 'csv', delimiter: 'asdf', extension: 'asf' }; From 6da22d69f866a3a53252263f4cccab4ca32a81e7 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 16:39:01 -0400 Subject: [PATCH 33/43] refactor: update index to use updated entity relationship diagram component --- packages/ui/stories/index.mdx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/ui/stories/index.mdx b/packages/ui/stories/index.mdx index 94e9af62..cfb8b4db 100644 --- a/packages/ui/stories/index.mdx +++ b/packages/ui/stories/index.mdx @@ -1,5 +1,10 @@ -import { Meta, Controls } from '@storybook/blocks'; -import { SchemaRelationshipDiagram } from '../src/viewer-diagram/SchemaRelationshipDiagram'; +import { Meta } from '@storybook/blocks'; +import { + ActiveRelationshipProvider, + buildRelationshipMap, + RelationshipDiagramContent, +} from '../src/viewer-table/EntityRelationshipDiagram'; +import { ThemeProvider } from '../src/theme'; import biosampleDictionary from './fixtures/minimalBiosampleModel'; @@ -8,10 +13,14 @@ import biosampleDictionary from './fixtures/minimalBiosampleModel'; ## Dictionary Viewer -### Entity Relation Diagram +### Entity Relationship Diagram
- + + + + +
### Table View From a51de9ea08ba1b91cce91bb14d3298a254136cb6 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 16:39:25 -0400 Subject: [PATCH 34/43] docs: add new entity relationship diagram stories --- .../EntityRelationshipDiagram.stories.tsx | 245 ++++++++++++++++++ .../SchemaDiagramNode.stories.tsx | 97 +++++++ 2 files changed, 342 insertions(+) create mode 100644 packages/ui/stories/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.stories.tsx create mode 100644 packages/ui/stories/viewer-table/EntityRelationshipDiagram/SchemaDiagramNode.stories.tsx diff --git a/packages/ui/stories/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.stories.tsx b/packages/ui/stories/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.stories.tsx new file mode 100644 index 00000000..e063ca03 --- /dev/null +++ b/packages/ui/stories/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.stories.tsx @@ -0,0 +1,245 @@ +/* + * + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import type { Dictionary } from '@overture-stack/lectern-dictionary'; +import { useMemo } from 'react'; + +import React from 'react'; +import { + ActiveRelationshipProvider, + buildRelationshipMap, + RelationshipDiagramContent, +} from '../../../src/viewer-table/EntityRelationshipDiagram'; +import DictionarySample from '../../fixtures/pcgl.json'; +import SimpleClinicalERDiagram from '../../fixtures/simpleClinicalERDiagram.json'; +import SingleSchemaFixture from '../../fixtures/singleSchema.json'; +import TwoIsolatedSchemasFixture from '../../fixtures/twoIsolatedSchemas.json'; +import TwoSchemaLinearFixture from '../../fixtures/twoSchemaLinear.json'; +import ThreeSchemaChainFixture from '../../fixtures/threeSchemaChain.json'; +import MultiFkFixture from '../../fixtures/multiFk.json'; +import FanOutFixture from '../../fixtures/fanOut.json'; +import MixedRelationsFixture from '../../fixtures/mixedRelations.json'; +import CompoundKeyFixture from '../../fixtures/compoundKey.json'; +import CyclicalFixture from '../../fixtures/cyclical.json'; +import InvalidUniqueKeyFixture from '../../fixtures/invalid_uniquekey.json'; +import themeDecorator from '../../themeDecorator'; + +const meta = { + component: RelationshipDiagramContent, + title: 'Viewer - Table/Entity Relationship Diagram/Entity Relationship Diagram', + tags: ['autodocs'], + decorators: [themeDecorator()], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Interactive entity relationship diagram that visualizes foreign key relationships between schemas. Schemas are rendered as nodes with fields, and edges connect foreign key fields to their referenced unique key fields. Clicking a foreign key field highlights the full relationship chain across schemas.', + }, + story: { + inline: false, + iframeHeight: 600, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const StoryWrapper = ({ dictionary }: { dictionary: Dictionary }) => { + const relationshipMap = useMemo(() => buildRelationshipMap(dictionary), [dictionary]); + return ( + + + + ); +}; + +/** + * Full PCGL dictionary with 17 schemas and multiple foreign key relationships. + */ +export const Default: Story = { + args: { + dictionary: DictionarySample as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * A simplified clinical data model with a handful of related schemas. + */ +export const SimpleClinicalExample: Story = { + args: { + dictionary: SimpleClinicalERDiagram as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * A dictionary with only one schema and no relationships. + */ +export const SingleSchema: Story = { + args: { + dictionary: SingleSchemaFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * Two schemas with no foreign key relationships between them. + */ +export const TwoIsolatedSchemas: Story = { + args: { + dictionary: TwoIsolatedSchemasFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * Two schemas connected by a single foreign key. + */ +export const TwoSchemaLinear: Story = { + args: { + dictionary: TwoSchemaLinearFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * Three schemas linked in a chain: A → B → C. + */ +export const ThreeSchemaChain: Story = { + args: { + dictionary: ThreeSchemaChainFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * A schema with multiple foreign keys referencing different schemas. + */ +export const MultiFk: Story = { + args: { + dictionary: MultiFkFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * One parent schema referenced by multiple child schemas (fan-out pattern). + */ +export const FanOut: Story = { + args: { + dictionary: FanOutFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * A mix of linear chains, fan-out, and isolated schemas. + */ +export const MixedRelations: Story = { + args: { + dictionary: MixedRelationsFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * A foreign key mapping that uses a compound (multi-field) key. + */ +export const CompoundKey: Story = { + args: { + dictionary: CompoundKeyFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * Schemas with cyclical foreign key references (A → B → A). + */ +export const Cyclical: Story = { + args: { + dictionary: CyclicalFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; + +/** + * A foreign key that references a field which is not a unique key, + * testing graceful handling of invalid configurations. + */ +export const NonUniqueForeignKey: Story = { + args: { + dictionary: InvalidUniqueKeyFixture as Dictionary, + }, + render: (args) => ( +
+ +
+ ), +}; diff --git a/packages/ui/stories/viewer-table/EntityRelationshipDiagram/SchemaDiagramNode.stories.tsx b/packages/ui/stories/viewer-table/EntityRelationshipDiagram/SchemaDiagramNode.stories.tsx new file mode 100644 index 00000000..1a770060 --- /dev/null +++ b/packages/ui/stories/viewer-table/EntityRelationshipDiagram/SchemaDiagramNode.stories.tsx @@ -0,0 +1,97 @@ +/* + * + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ +/** @jsxImportSource @emotion/react */ + +import type { Dictionary, Schema } from '@overture-stack/lectern-dictionary'; +import type { Meta, StoryObj } from '@storybook/react'; +import { useMemo } from 'react'; +import { ReactFlowProvider } from 'reactflow'; + +import { SchemaNode } from '../../../src/viewer-table/EntityRelationshipDiagram/SchemaNode'; +import { + ActiveRelationshipProvider, + buildRelationshipMap, +} from '../../../src/viewer-table/EntityRelationshipDiagram'; + +import DictionarySample from '../../fixtures/pcgl.json'; +import websiteUserDictionary from '../../fixtures/websiteUsersDataDictionary'; +import themeDecorator from '../../themeDecorator'; + +const SchemaNodeWrapper = ({ schema, dictionary }: { schema: Schema; dictionary: Dictionary }) => { + const relationshipMap = useMemo(() => buildRelationshipMap(dictionary), [dictionary]); + return ( + + +
+
+ +
+
+
+
+ ); +}; + +const meta = { + component: SchemaNode, + title: 'Viewer - Table/Entity Relationship Diagram/Schema Diagram Node', + tags: ['autodocs'], + decorators: [themeDecorator()], + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Individual schema node rendered inside the entity relationship diagram. Displays the schema name, its fields, and key indicators (unique key, foreign key) with data type badges. Foreign key fields are interactive and highlight the related relationship chain when clicked.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const pcglDictionary = DictionarySample as Dictionary; + +/** + * A basic schema with standard fields and a unique key, but no foreign key relationships. + */ +export const SimpleSchema: Story = { + args: { data: websiteUserDictionary.schemas[0] as Schema }, + render: (args) => , +}; + +/** + * A schema that contains foreign key fields referencing another schema, + * shown with key indicators and interactive highlighting. + */ +export const WithForeignKeys: Story = { + args: { data: pcglDictionary.schemas.find((s) => s.name === 'diagnosis') as Schema }, + render: (args) => , +}; + +/** + * A schema with a large number of fields, demonstrating how the node scales vertically. + */ +export const ManyFields: Story = { + args: { data: pcglDictionary.schemas.find((s) => s.name === 'treatment') as Schema }, + render: (args) => , +}; From 7566ffc1002e46633a7e2707c95606ce7a34eea2 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 16:39:36 -0400 Subject: [PATCH 35/43] docs: add new filter dropdown stories --- .../Toolbar/FilterDropdowns.stories.tsx | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx diff --git a/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx new file mode 100644 index 00000000..252d7b11 --- /dev/null +++ b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx @@ -0,0 +1,132 @@ +/* + * + * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved + * + * This program and the accompanying materials are made available under the terms of + * the GNU Affero General Public License v3.0. You should have received a copy of the + * GNU Affero General Public License along with this program. + * If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/** @jsxImportSource @emotion/react */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/test'; + +import { DictionaryTableViewer } from '../../../src/viewer-table/DictionaryTableViewer'; + +import { withFilterDictionary, withMultipleDictionaries } from '../../dictionaryDecorator'; +import themeDecorator from '../../themeDecorator'; + +const meta = { + component: DictionaryTableViewer, + title: 'Viewer - Table/Toolbar/Filter Dropdowns', + tags: ['autodocs'], + decorators: [themeDecorator()], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Stories demonstrating the optional filter dropdown feature, which allows schemas to be filtered by metadata properties and displays tag pills on accordion headers.', + }, + story: { + inline: false, + iframeHeight: 600, + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** + * Default view with two filter categories configured. + * Tag pills are visible on each accordion header showing metadata values, + * with no filters active. + */ +export const Default: Story = { + decorators: [withFilterDictionary], + args: { + filterDropdowns: [ + { label: 'Submitter', filterProperty: 'meta.submitter' }, + { label: 'Domain', filterProperty: 'meta.domain' }, + ], + }, +}; + +/** + * A single filter category configured. The toolbar button displays the + * filter label directly instead of a generic "Filters" label. + */ +export const SingleFilter: Story = { + decorators: [withFilterDictionary], + args: { + filterDropdowns: [{ label: 'Submitter', filterProperty: 'meta.submitter' }], + }, +}; + +/** + * Opens the filter dropdown and selects Submitter: "Clinician". + * Only schemas with submitter "Clinician" are shown. + */ +export const FilterSelected: Story = { + decorators: [withFilterDictionary], + args: { + filterDropdowns: [ + { label: 'Submitter', filterProperty: 'meta.submitter' }, + { label: 'Domain', filterProperty: 'meta.domain' }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const filtersButton = await canvas.findByRole('button', { name: /Filters/i }); + await userEvent.click(filtersButton); + const clinicianCheckbox = await canvas.findByLabelText('Clinician'); + await userEvent.click(clinicianCheckbox); + }, +}; + +/** + * Opens the filter dropdown and selects Submitter: "Researcher" and + * Domain: "Health". No schemas match both filters, so the empty state + * is displayed. + */ +export const EmptyState: Story = { + decorators: [withFilterDictionary], + args: { + filterDropdowns: [ + { label: 'Submitter', filterProperty: 'meta.submitter' }, + { label: 'Domain', filterProperty: 'meta.domain' }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const filtersButton = await canvas.findByRole('button', { name: /Filters/i }); + await userEvent.click(filtersButton); + const researcherCheckbox = await canvas.findByLabelText('Researcher'); + await userEvent.click(researcherCheckbox); + const healthCheckbox = await canvas.findByLabelText('Health'); + await userEvent.click(healthCheckbox); + }, +}; + +/** + * No filterDropdowns configured — the component renders without any + * filter controls or tag pills, confirming no regressions to existing behaviour. + */ +export const NoConfig: Story = { + decorators: [withMultipleDictionaries], +}; From f3dff1b97b40c108560c2841eee6533b552cf6c0 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 16:40:16 -0400 Subject: [PATCH 36/43] refactor: remove unnecessary viewport testing story --- .../DictionaryViewerViewport.stories.tsx | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 packages/ui/stories/viewer-table/DictionaryViewerViewport.stories.tsx diff --git a/packages/ui/stories/viewer-table/DictionaryViewerViewport.stories.tsx b/packages/ui/stories/viewer-table/DictionaryViewerViewport.stories.tsx deleted file mode 100644 index 0408bb96..00000000 --- a/packages/ui/stories/viewer-table/DictionaryViewerViewport.stories.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * - * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved - * - * This program and the accompanying materials are made available under the terms of - * the GNU Affero General Public License v3.0. You should have received a copy of the - * GNU Affero General Public License along with this program. - * If not, see . - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -/** @jsxImportSource @emotion/react */ - -import type { Meta, StoryObj } from '@storybook/react'; - -import { DictionaryTableViewer } from '../../src/viewer-table/DictionaryTableViewer'; -import { withMultipleDictionaries } from '../dictionaryDecorator'; -import themeDecorator from '../themeDecorator'; - -const meta = { - component: DictionaryTableViewer, - title: 'Viewer - Table/Viewport Testing', - decorators: [themeDecorator(), withMultipleDictionaries], - parameters: { - layout: 'padded-fullscreen', - padding: 64, - docs: { disable: true }, // Hide from docs, this is for manual testing - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; - -export const Tablet: Story = { - parameters: { - viewport: { defaultViewport: 'tablet' }, - }, -}; From 92f543b2ae6653e5c44bbc8fa6231cfe858a664f Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 17:18:13 -0400 Subject: [PATCH 37/43] fix: remove filter stories dictionary viewer page story --- .../DictionaryViewerPage.stories.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx index fa8aed64..59428af3 100644 --- a/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx +++ b/packages/ui/stories/viewer-table/DictionaryViewerPage.stories.tsx @@ -32,7 +32,6 @@ import { withLecternUrl, withMultipleDictionaries, withSingleDictionary, - withFilterDictionary, } from '../dictionaryDecorator'; import themeDecorator from '../themeDecorator'; @@ -95,19 +94,3 @@ export const LecternServer: Story = { decorators: [withLecternUrl()], }; -export const WithSingleFilter: Story = { - decorators: [withFilterDictionary], - args: { - filterDropdowns: [{ label: 'Submitter', filterProperty: 'meta.submitter' }], - }, -}; - -export const WithFilterDropdowns: Story = { - decorators: [withFilterDictionary], - args: { - filterDropdowns: [ - { label: 'Submitter', filterProperty: 'meta.submitter' }, - { label: 'Domain', filterProperty: 'meta.domain' }, - ], - }, -}; From 71da347281fab3055c8ea5684108fe1bceff0d19 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 17:18:39 -0400 Subject: [PATCH 38/43] refactor: remove old entityrelationship diagram story --- .../EntityRelationshipDiagram.stories.tsx | 197 ------------------ 1 file changed, 197 deletions(-) delete mode 100644 packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx diff --git a/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx b/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx deleted file mode 100644 index 56348a6f..00000000 --- a/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx +++ /dev/null @@ -1,197 +0,0 @@ -/* - * - * Copyright (c) 2026 The Ontario Institute for Cancer Research. All rights reserved - * - * This program and the accompanying materials are made available under the terms of - * the GNU Affero General Public License v3.0. You should have received a copy of the - * GNU Affero General Public License along with this program. - * If not, see . - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN - * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -import type { Meta, StoryObj } from '@storybook/react'; -import type { Dictionary } from '@overture-stack/lectern-dictionary'; -import { useMemo } from 'react'; - -import React from 'react'; -import { - ActiveRelationshipProvider, - buildRelationshipMap, - RelationshipDiagramContent, -} from '../../src/viewer-table/EntityRelationshipDiagram'; -import DictionarySample from '../fixtures/pcgl.json'; -import SimpleClinicalERDiagram from '../fixtures/simpleClinicalERDiagram.json'; -import SingleSchemaFixture from '../fixtures/singleSchema.json'; -import TwoIsolatedSchemasFixture from '../fixtures/twoIsolatedSchemas.json'; -import TwoSchemaLinearFixture from '../fixtures/twoSchemaLinear.json'; -import ThreeSchemaChainFixture from '../fixtures/threeSchemaChain.json'; -import MultiFkFixture from '../fixtures/multiFk.json'; -import FanOutFixture from '../fixtures/fanOut.json'; -import MixedRelationsFixture from '../fixtures/mixedRelations.json'; -import CompoundKeyFixture from '../fixtures/compoundKey.json'; -import CyclicalFixture from '../fixtures/cyclical.json'; -import InvalidUniqueKeyFixture from '../fixtures/invalid_uniquekey.json'; -import themeDecorator from '../themeDecorator'; - -const meta = { - component: RelationshipDiagramContent, - title: 'Viewer - Table/Entity Relationship Diagram', - decorators: [themeDecorator()], - parameters: { - layout: 'fullscreen', - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -const StoryWrapper = ({ dictionary }: { dictionary: Dictionary }) => { - const relationshipMap = useMemo(() => buildRelationshipMap(dictionary), [dictionary]); - return ( - - - - ); -}; - -export const Default: Story = { - args: { - dictionary: DictionarySample as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const SimpleClinicalExample: Story = { - args: { - dictionary: SimpleClinicalERDiagram as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const SingleSchema: Story = { - args: { - dictionary: SingleSchemaFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const TwoIsolatedSchemas: Story = { - args: { - dictionary: TwoIsolatedSchemasFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const TwoSchemaLinear: Story = { - args: { - dictionary: TwoSchemaLinearFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const ThreeSchemaChain: Story = { - args: { - dictionary: ThreeSchemaChainFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const MultiFk: Story = { - args: { - dictionary: MultiFkFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const FanOut: Story = { - args: { - dictionary: FanOutFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const MixedRelations: Story = { - args: { - dictionary: MixedRelationsFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const CompoundKey: Story = { - args: { - dictionary: CompoundKeyFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const Cyclical: Story = { - args: { - dictionary: CyclicalFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; - -export const NonUniqueForeignKey: Story = { - args: { - dictionary: InvalidUniqueKeyFixture as Dictionary, - }, - render: (args) => ( -
- -
- ), -}; \ No newline at end of file From 820f805949e1674357034fbcd3a9fc0161be7036 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 17:19:18 -0400 Subject: [PATCH 39/43] refactor: set auto play to false to prevent scrolling to empty filter story when opening up docs story --- .../Toolbar/FilterDropdowns.stories.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx index 252d7b11..ec04aabd 100644 --- a/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx @@ -42,6 +42,7 @@ const meta = { 'Stories demonstrating the optional filter dropdown feature, which allows schemas to be filtered by metadata properties and displays tag pills on accordion headers.', }, story: { + autoplay: false, inline: false, iframeHeight: 600, }, @@ -90,9 +91,17 @@ export const FilterSelected: Story = { { label: 'Domain', filterProperty: 'meta.domain' }, ], }, + parameters: { + docs: { + story: { + inline: true, + autoplay: false, + }, + }, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const filtersButton = await canvas.findByRole('button', { name: /Filters/i }); + const filtersButton = await canvas.findByRole('button', { name: 'Filters' }); await userEvent.click(filtersButton); const clinicianCheckbox = await canvas.findByLabelText('Clinician'); await userEvent.click(clinicianCheckbox); @@ -112,9 +121,17 @@ export const EmptyState: Story = { { label: 'Domain', filterProperty: 'meta.domain' }, ], }, + parameters: { + docs: { + story: { + inline: true, + autoplay: false, + }, + }, + }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const filtersButton = await canvas.findByRole('button', { name: /Filters/i }); + const filtersButton = await canvas.findByRole('button', { name: 'Filters' }); await userEvent.click(filtersButton); const researcherCheckbox = await canvas.findByLabelText('Researcher'); await userEvent.click(researcherCheckbox); From 96af93e8f3769685375cb11031530c27fb92f178 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 21:04:28 -0400 Subject: [PATCH 40/43] style: adjust comments and add fixed height to stories with play functions --- .../Toolbar/FilterDropdowns.stories.tsx | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx index ec04aabd..0951bec9 100644 --- a/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx @@ -21,7 +21,7 @@ /** @jsxImportSource @emotion/react */ -import type { Meta, StoryObj } from '@storybook/react'; +import type { Decorator, Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/test'; import { DictionaryTableViewer } from '../../../src/viewer-table/DictionaryTableViewer'; @@ -29,6 +29,12 @@ import { DictionaryTableViewer } from '../../../src/viewer-table/DictionaryTable import { withFilterDictionary, withMultipleDictionaries } from '../../dictionaryDecorator'; import themeDecorator from '../../themeDecorator'; +const withFixedHeight: Decorator = (Story) => ( +
+ +
+); + const meta = { component: DictionaryTableViewer, title: 'Viewer - Table/Toolbar/Filter Dropdowns', @@ -39,7 +45,7 @@ const meta = { docs: { description: { component: - 'Stories demonstrating the optional filter dropdown feature, which allows schemas to be filtered by metadata properties and displays tag pills on accordion headers.', + 'Stories demonstrating the optional filter dropdown feature, which allows schemas to be filtered by metadata properties.', }, story: { autoplay: false, @@ -55,8 +61,6 @@ type Story = StoryObj; /** * Default view with two filter categories configured. - * Tag pills are visible on each accordion header showing metadata values, - * with no filters active. */ export const Default: Story = { decorators: [withFilterDictionary], @@ -80,11 +84,11 @@ export const SingleFilter: Story = { }; /** - * Opens the filter dropdown and selects Submitter: "Clinician". - * Only schemas with submitter "Clinician" are shown. + * Selects Submitter: "Clinician" to show only matching schemas. + * Navigate to the canvas tab to see a live demo. */ export const FilterSelected: Story = { - decorators: [withFilterDictionary], + decorators: [withFixedHeight, withFilterDictionary], args: { filterDropdowns: [ { label: 'Submitter', filterProperty: 'meta.submitter' }, @@ -109,12 +113,11 @@ export const FilterSelected: Story = { }; /** - * Opens the filter dropdown and selects Submitter: "Researcher" and - * Domain: "Health". No schemas match both filters, so the empty state - * is displayed. + * Selects Submitter: "Researcher" and Domain: "Health" and shows the empty state, + * navigate to the canvas tab to see a live demo. */ export const EmptyState: Story = { - decorators: [withFilterDictionary], + decorators: [withFixedHeight, withFilterDictionary], args: { filterDropdowns: [ { label: 'Submitter', filterProperty: 'meta.submitter' }, @@ -141,8 +144,8 @@ export const EmptyState: Story = { }; /** - * No filterDropdowns configured — the component renders without any - * filter controls or tag pills, confirming no regressions to existing behaviour. + * No filterDropdowns configured, the component renders without any + * filter controls. */ export const NoConfig: Story = { decorators: [withMultipleDictionaries], From 635906d7163e0c3ee0a685c968e9f202a194fe92 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Wed, 25 Mar 2026 21:04:57 -0400 Subject: [PATCH 41/43] style: add descriptions to stories --- packages/ui/stories/common/Accordion.stories.tsx | 7 +++++++ packages/ui/stories/common/Button.stories.tsx | 7 +++++++ packages/ui/stories/common/DropDown.stories.tsx | 7 +++++++ packages/ui/stories/common/FieldBlock.stories.tsx | 7 +++++++ packages/ui/stories/common/ListItem.stories.tsx | 7 +++++++ packages/ui/stories/common/Pill.stories.tsx | 7 +++++++ .../Conditional-modal/ConditionalBlock.stories.tsx | 7 +++++++ .../Conditional-modal/ConditionalLogicModal.stories.tsx | 7 +++++++ .../ConditionalRestrictionsDetails.stories.tsx | 7 +++++++ .../viewer-table/Conditional-modal/Description.stories.tsx | 7 +++++++ .../viewer-table/Conditional-modal/IfStatement.stories.tsx | 7 +++++++ .../Conditional-modal/RenderAllowedValues.stories.tsx | 7 +++++++ .../DictionaryHeader/DictionaryVersionSwitcher.stories.tsx | 7 +++++++ packages/ui/stories/viewer-table/SchemaTable.stories.tsx | 7 +++++++ .../viewer-table/Toolbar/CollapseAllButton.stories.tsx | 7 +++++++ .../viewer-table/Toolbar/DiagramViewButton.stories.tsx | 7 +++++++ .../Toolbar/DictionaryDownloadButton.stories.tsx | 7 +++++++ .../viewer-table/Toolbar/ExpandAllButton.stories.tsx | 7 +++++++ .../Toolbar/TableOfContentDropdown.stories.tsx | 7 +++++++ 19 files changed, 133 insertions(+) diff --git a/packages/ui/stories/common/Accordion.stories.tsx b/packages/ui/stories/common/Accordion.stories.tsx index 2c727b95..e291f92f 100644 --- a/packages/ui/stories/common/Accordion.stories.tsx +++ b/packages/ui/stories/common/Accordion.stories.tsx @@ -33,6 +33,13 @@ const meta = { title: 'Common/Accordion', tags: ['autodocs'], decorators: [themeDecorator(), withLecternUrl()], + parameters: { + docs: { + description: { + component: 'Accordion component for displaying collapsible content sections.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/common/Button.stories.tsx b/packages/ui/stories/common/Button.stories.tsx index 5710fffa..24103019 100644 --- a/packages/ui/stories/common/Button.stories.tsx +++ b/packages/ui/stories/common/Button.stories.tsx @@ -33,6 +33,13 @@ const meta = { title: 'Common/Button', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'A themed button component with support for icons, disabled state, and custom styling.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/common/DropDown.stories.tsx b/packages/ui/stories/common/DropDown.stories.tsx index 8378738b..7ee4ecc3 100644 --- a/packages/ui/stories/common/DropDown.stories.tsx +++ b/packages/ui/stories/common/DropDown.stories.tsx @@ -33,6 +33,13 @@ const meta = { title: 'Common/Dropdown', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'Dropdown component with toggle button and collapsible menu.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/common/FieldBlock.stories.tsx b/packages/ui/stories/common/FieldBlock.stories.tsx index 6deed98e..ae883387 100644 --- a/packages/ui/stories/common/FieldBlock.stories.tsx +++ b/packages/ui/stories/common/FieldBlock.stories.tsx @@ -30,6 +30,13 @@ const meta = { title: 'Common/FieldBlock', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'A styled container for rendering field content within schema tables.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/common/ListItem.stories.tsx b/packages/ui/stories/common/ListItem.stories.tsx index 90e79697..ff651778 100644 --- a/packages/ui/stories/common/ListItem.stories.tsx +++ b/packages/ui/stories/common/ListItem.stories.tsx @@ -31,6 +31,13 @@ const meta = { title: 'Common/ListItem', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'A styled list item used to display individual values such as allowed values or enum entries.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/common/Pill.stories.tsx b/packages/ui/stories/common/Pill.stories.tsx index 65c91ad0..d6e88f23 100644 --- a/packages/ui/stories/common/Pill.stories.tsx +++ b/packages/ui/stories/common/Pill.stories.tsx @@ -32,6 +32,13 @@ const meta = { title: 'Common/Pill', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'A compact label used to display tags, metadata values, or status indicators.', + }, + }, + }, argTypes: { size: { control: { type: 'select' }, diff --git a/packages/ui/stories/viewer-table/Conditional-modal/ConditionalBlock.stories.tsx b/packages/ui/stories/viewer-table/Conditional-modal/ConditionalBlock.stories.tsx index 39789cf1..29375125 100644 --- a/packages/ui/stories/viewer-table/Conditional-modal/ConditionalBlock.stories.tsx +++ b/packages/ui/stories/viewer-table/Conditional-modal/ConditionalBlock.stories.tsx @@ -29,6 +29,13 @@ const meta = { title: 'Viewer Table/Conditional Modal/ConditionalBlock', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'Renders a block of conditional restriction statements showing field conditions and their resulting restrictions.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Conditional-modal/ConditionalLogicModal.stories.tsx b/packages/ui/stories/viewer-table/Conditional-modal/ConditionalLogicModal.stories.tsx index 688110ec..a79ec290 100644 --- a/packages/ui/stories/viewer-table/Conditional-modal/ConditionalLogicModal.stories.tsx +++ b/packages/ui/stories/viewer-table/Conditional-modal/ConditionalLogicModal.stories.tsx @@ -32,6 +32,13 @@ const meta = { title: 'Viewer Table/Conditional Modal/ConditionalLogicModal', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'A modal dialog that displays the full conditional logic for a schema field, including if/then restriction rules.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Conditional-modal/ConditionalRestrictionsDetails.stories.tsx b/packages/ui/stories/viewer-table/Conditional-modal/ConditionalRestrictionsDetails.stories.tsx index f89fb13f..09a7eac5 100644 --- a/packages/ui/stories/viewer-table/Conditional-modal/ConditionalRestrictionsDetails.stories.tsx +++ b/packages/ui/stories/viewer-table/Conditional-modal/ConditionalRestrictionsDetails.stories.tsx @@ -41,6 +41,13 @@ const meta = { title: 'Viewer - Table/Conditional Modal/Rendering Condition Logic', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'Renders the condition details for a conditional restriction, showing field match criteria and match case logic.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Conditional-modal/Description.stories.tsx b/packages/ui/stories/viewer-table/Conditional-modal/Description.stories.tsx index dadd1eba..15446f82 100644 --- a/packages/ui/stories/viewer-table/Conditional-modal/Description.stories.tsx +++ b/packages/ui/stories/viewer-table/Conditional-modal/Description.stories.tsx @@ -33,6 +33,13 @@ const meta = { title: 'Viewer - Table/Conditional Modal/Description', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'Displays a human-readable description of a schema field, including its name, type, and restrictions.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Conditional-modal/IfStatement.stories.tsx b/packages/ui/stories/viewer-table/Conditional-modal/IfStatement.stories.tsx index 55c21e83..ad042968 100644 --- a/packages/ui/stories/viewer-table/Conditional-modal/IfStatement.stories.tsx +++ b/packages/ui/stories/viewer-table/Conditional-modal/IfStatement.stories.tsx @@ -29,6 +29,13 @@ const meta = { title: 'Viewer Table/Conditional Modal/If Statement', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'Renders an if/then conditional restriction, showing the conditions that must be met and the resulting field restrictions.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Conditional-modal/RenderAllowedValues.stories.tsx b/packages/ui/stories/viewer-table/Conditional-modal/RenderAllowedValues.stories.tsx index 61292063..3c1bf635 100644 --- a/packages/ui/stories/viewer-table/Conditional-modal/RenderAllowedValues.stories.tsx +++ b/packages/ui/stories/viewer-table/Conditional-modal/RenderAllowedValues.stories.tsx @@ -33,6 +33,13 @@ const meta = { title: 'Viewer Table/Conditional Modal/Render Allowed Values', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'Displays the allowed values for a field restriction as a formatted list.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.stories.tsx b/packages/ui/stories/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.stories.tsx index bce09614..a461db3d 100644 --- a/packages/ui/stories/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.stories.tsx +++ b/packages/ui/stories/viewer-table/DictionaryHeader/DictionaryVersionSwitcher.stories.tsx @@ -35,6 +35,13 @@ const meta = { component: VersionSwitcher, title: 'Viewer - Table/DictionaryHeader/Dictionary Version Switcher', tags: ['autodocs'], + parameters: { + docs: { + description: { + component: 'A dropdown that allows switching between available dictionary versions.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/SchemaTable.stories.tsx b/packages/ui/stories/viewer-table/SchemaTable.stories.tsx index 8631fa0d..eeddb3cf 100644 --- a/packages/ui/stories/viewer-table/SchemaTable.stories.tsx +++ b/packages/ui/stories/viewer-table/SchemaTable.stories.tsx @@ -37,6 +37,13 @@ const meta = { title: 'Viewer - Table/Schema Table', tags: ['autodocs'], decorators: [themeDecorator()], + parameters: { + docs: { + description: { + component: 'Renders a single schema as a table displaying field names, types, restrictions, and descriptions.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Toolbar/CollapseAllButton.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/CollapseAllButton.stories.tsx index 6daa0c69..23909f96 100644 --- a/packages/ui/stories/viewer-table/Toolbar/CollapseAllButton.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/CollapseAllButton.stories.tsx @@ -33,6 +33,13 @@ const meta = { title: 'Viewer - Table/Toolbar/CollapseAllButton', tags: ['autodocs'], decorators: [themeDecorator(), withMultipleDictionaries], + parameters: { + docs: { + description: { + component: 'A toolbar button that collapses all open accordion sections.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx index 6879d9f7..ae7ebb1f 100644 --- a/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/DiagramViewButton.stories.tsx @@ -31,6 +31,13 @@ const meta = { title: 'Viewer - Table/Toolbar/DiagramViewButton', tags: ['autodocs'], decorators: [themeDecorator(), withDictionaryContext(emptyDictionaryData)], + parameters: { + docs: { + description: { + component: 'A toolbar button that toggles between the table view and the entity relationship diagram view.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Toolbar/DictionaryDownloadButton.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/DictionaryDownloadButton.stories.tsx index 7d21a918..d9d32e2f 100644 --- a/packages/ui/stories/viewer-table/Toolbar/DictionaryDownloadButton.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/DictionaryDownloadButton.stories.tsx @@ -32,6 +32,13 @@ const meta = { title: 'Viewer - Table/Toolbar/DictionaryDownloadButton', tags: ['autodocs'], decorators: [themeDecorator(), withDictionaryContext()], + parameters: { + docs: { + description: { + component: 'A toolbar button that downloads the current dictionary as a JSON file.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Toolbar/ExpandAllButton.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/ExpandAllButton.stories.tsx index 13e51c34..d525ba7c 100644 --- a/packages/ui/stories/viewer-table/Toolbar/ExpandAllButton.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/ExpandAllButton.stories.tsx @@ -33,6 +33,13 @@ const meta = { title: 'Viewer - Table/Toolbar/ExpandAllButton', tags: ['autodocs'], decorators: [themeDecorator(), withMultipleDictionaries], + parameters: { + docs: { + description: { + component: 'A toolbar button that expands all collapsed accordion sections.', + }, + }, + }, } satisfies Meta; export default meta; diff --git a/packages/ui/stories/viewer-table/Toolbar/TableOfContentDropdown.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/TableOfContentDropdown.stories.tsx index e0fbdfd2..58e2afd6 100644 --- a/packages/ui/stories/viewer-table/Toolbar/TableOfContentDropdown.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/TableOfContentDropdown.stories.tsx @@ -34,6 +34,13 @@ const meta = { title: 'Viewer - Table/Toolbar/Table of Contents Dropdown', tags: ['autodocs'], decorators: [themeDecorator(), withMultipleDictionaries], + parameters: { + docs: { + description: { + component: 'A dropdown menu listing all schemas in the dictionary for quick navigation to a specific section.', + }, + }, + }, } satisfies Meta; export default meta; From 3cfd568afc56d58780fc186ee4cfc1904fc1a6f5 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 26 Mar 2026 10:37:07 -0400 Subject: [PATCH 42/43] refactor: delete legacy schema relationship diagram code --- .../src/viewer-diagram/SchemaDiagramNode.tsx | 220 ------------------ .../ui/src/viewer-diagram/SchemaFlowNode.ts | 56 ----- .../SchemaRelationshipDiagram.tsx | 91 -------- packages/ui/src/viewer-diagram/index.ts | 1 - 4 files changed, 368 deletions(-) delete mode 100644 packages/ui/src/viewer-diagram/SchemaDiagramNode.tsx delete mode 100644 packages/ui/src/viewer-diagram/SchemaFlowNode.ts delete mode 100644 packages/ui/src/viewer-diagram/SchemaRelationshipDiagram.tsx delete mode 100644 packages/ui/src/viewer-diagram/index.ts diff --git a/packages/ui/src/viewer-diagram/SchemaDiagramNode.tsx b/packages/ui/src/viewer-diagram/SchemaDiagramNode.tsx deleted file mode 100644 index 911edf9c..00000000 --- a/packages/ui/src/viewer-diagram/SchemaDiagramNode.tsx +++ /dev/null @@ -1,220 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { css } from '@emotion/react'; -import type { Schema } from '@overture-stack/lectern-dictionary'; -import { Handle, Position } from 'reactflow'; -import 'reactflow/dist/style.css'; -import { isFieldRequired } from '../utils/isFieldRequired'; - -// TODO: Colors and styling constants taken from theme instead of hardcoded - -const baseFieldRowStyles = { - padding: '8px 12px', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - borderBottom: '1px solid #e2e8f0', - transition: 'background-color 0.2s', -}; - -const primaryKeyRowStyles = css({ - ...baseFieldRowStyles, - backgroundColor: '#fef3c7', - borderLeft: '4px solid #f59e0b', -}); -const foreignKeyRowStyles = css({ - ...baseFieldRowStyles, - backgroundColor: '#d1fae5', - borderLeft: '4px solid #10b981', -}); -const standardKeyRowStyles = css({ - ...baseFieldRowStyles, - '&:hover': { - backgroundColor: '#e0f2fe', - }, - '&:last-child': { - borderBottom: 'none', - }, -}); - -const fieldContentStyles = css({ - display: 'flex', - alignItems: 'center', - gap: '8px', - flex: 1, - minWidth: 0, -}); - -const keyIndicatorStyles = css({ - display: 'flex', - alignItems: 'center', - gap: '4px', - flexShrink: 0, -}); - -const primaryKeyBadgeStyles = css({ - color: '#d97706', - fontWeight: 'bold', - fontSize: '14px', -}); - -const foreignKeyBadgeStyles = css({ - color: '#059669', - fontWeight: 'bold', - fontSize: '14px', -}); - -const requiredFieldStyles = css({ - color: '#dc2626', - fontSize: '14px', -}); - -const baseFieldNameStyles = css({ - fontWeight: 'bold', - fontSize: '14px', - color: '#1f2937', - whiteSpace: 'nowrap' as const, - overflow: 'hidden', - textOverflow: 'ellipsis', - flex: 1, -}); - -const primaryKeyFieldStyles = css({ - ...baseFieldNameStyles, - fontWeight: '900', - color: '#111827', -}); - -const dataTypeBadgeStyles = css({ - fontSize: '12px', - fontFamily: '"Monaco", "Consolas", "Ubuntu Mono", monospace', - backgroundColor: '#e5e7eb', - color: '#374151', - padding: '4px 8px', - borderRadius: '4px', - flexShrink: 0, - marginLeft: '8px', -}); - -/** - * Custom Schema Node Component - * Renders a Lectern schema as a clean, traditional ERD table - */ -export function SchemaDiagramNode(props: { data: Schema }) { - const { data: schema } = props; - - return ( -
- {/* Schema name header - prominent ERD style */} -
- {/*
*/} - {schema.name} - {/*
*/} -
- - {/* Fields list - table-like appearance */} -
- {schema.fields.map((field, index) => { - const isRequired = isFieldRequired(field); - - const isUniqueKey = schema.restrictions?.uniqueKey?.includes(field.name) || false; - const isForeignKey = - schema.restrictions?.foreignKey?.some((fk) => - fk.mappings.some((mapping) => mapping.local === field.name), - ) || false; - const rowStyles = - isUniqueKey ? primaryKeyRowStyles - : isForeignKey ? foreignKeyRowStyles - : standardKeyRowStyles; - - const valueType = field.isArray ? `${field.valueType}[]` : field.valueType; - - return ( -
-
- {/* Key indicators */} -
- {isUniqueKey && ( - - UK - - )} - {isForeignKey && ( - - FK - - )} - {isRequired && ( - - * - - )} -
- - {/* Field name */} - {field.name} -
- - {/* Data type badge */} - {valueType} -
- ); - })} -
- - {/* Connection handles - minimal and clean */} - - -
- ); -} diff --git a/packages/ui/src/viewer-diagram/SchemaFlowNode.ts b/packages/ui/src/viewer-diagram/SchemaFlowNode.ts deleted file mode 100644 index 03dac765..00000000 --- a/packages/ui/src/viewer-diagram/SchemaFlowNode.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { Dictionary, Schema, SchemaField } from '@overture-stack/lectern-dictionary'; -import type { Node } from 'reactflow'; - -export type SchemaFlowNodeField = SchemaField & { - isUniqueKey: boolean; - isRequired: boolean; - isForeignKey: boolean; -}; - -// export type SchemaFlowNode = Node & { fields: SchemaFlowNodeField[] }, 'schema'>; -export type SchemaFlowNode = Node; - -export type SchemaNodeLayout = { maxColumns: number; columnWidth: number; rowHeight: number }; - -/** - * Create a Node for ReactFlow to render from the data in a Schema. - * This runs a calculation on each field to determine if it is a uniqueKey or foreignKey for the schema - * @param schema - * @returns - */ -export function buildSchemaNode(schema: Schema): Omit { - return { - id: schema.name, - type: 'schema', - data: { ...schema }, - }; -} - -/** - * Given a dicitonary, generate an array of ReactFlow nodes from the schemas, including initial sizes and positions. - * - * Schemas are arranged in a grid, 4 wide. - */ -export function getNodesForDictionary(dictionary: Dictionary, layout?: Partial): Node[] { - const maxColumns = layout?.maxColumns ?? 4; - const columnWidth = layout?.columnWidth ?? 500; - const rowWidth = layout?.rowHeight ?? 500; - - // Create nodes for each schema with left-to-right flow positioning - return dictionary.schemas.map((schema, index) => { - const partialNode = buildSchemaNode(schema); - - // Determine Position of this node (top left corner) - const row = Math.floor(index / maxColumns); - const col = index % maxColumns; - - // Calculate positions for left-to-right flow - // TODO: calculate size based off node contents - const position: Node['position'] = { - x: col * columnWidth, - y: row * rowWidth, - }; - - return { ...partialNode, position }; - }); -} diff --git a/packages/ui/src/viewer-diagram/SchemaRelationshipDiagram.tsx b/packages/ui/src/viewer-diagram/SchemaRelationshipDiagram.tsx deleted file mode 100644 index b72bcc4c..00000000 --- a/packages/ui/src/viewer-diagram/SchemaRelationshipDiagram.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import type { Dictionary } from '@overture-stack/lectern-dictionary'; -import ReactFlow, { - Background, - BackgroundVariant, - Controls, - Edge, - MarkerType, - useEdgesState, - useNodesState, - type NodeTypes, -} from 'reactflow'; -import 'reactflow/dist/style.css'; -import { SchemaDiagramNode } from './SchemaDiagramNode'; -import { getNodesForDictionary, type SchemaNodeLayout } from './SchemaFlowNode'; - -// TODO: Colors and styling constants taken from theme instead of hardcoded - -// Register our custom node type -const nodeTypes: NodeTypes = { - schema: SchemaDiagramNode, -}; - -function getEdgesForDictionary(dictionary: Dictionary): Edge[] { - return dictionary.schemas - .flatMap((schema) => { - if (schema.restrictions?.foreignKey) { - return schema.restrictions.foreignKey.map((foreignKey) => { - const id = `${schema.name}-${foreignKey.schema}`; - const label = foreignKey.mappings.map((mapping) => `${mapping.local} → ${mapping.foreign}`).join(', '); - - return { - id, - label, - source: foreignKey.schema, - target: schema.name, - sourceHandle: 'source-right', - targetHandle: 'target-left', - type: 'smoothstep', - style: { stroke: '#374151', strokeWidth: 2 }, - labelStyle: { - fontSize: 11, - fontWeight: 'bold', - fill: '#374151', - backgroundColor: '#ffffff', - }, - labelBgStyle: { - fill: '#ffffff', - fillOpacity: 0.95, - stroke: '#e5e7eb', - strokeWidth: 1, - }, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 20, - height: 20, - color: '#374151', - }, - }; - }); - } - return undefined; - }) - .filter((maybeEdge) => maybeEdge !== undefined); -} - -export function SchemaRelationshipDiagram(props: { dictionary: Dictionary; layout?: Partial }) { - // const nodes = useMemo(() => getNodesForDictionary(props.dictionary), [props.dictionary]); - // const edges = getEdgesForDictionary(props.dictionary); - - const [nodes, setNodes, onNodesChange] = useNodesState(getNodesForDictionary(props.dictionary, props.layout)); - const [edges, setEdges, onEdgesChange] = useEdgesState(getEdgesForDictionary(props.dictionary)); - return ( - - - - - ); -} diff --git a/packages/ui/src/viewer-diagram/index.ts b/packages/ui/src/viewer-diagram/index.ts deleted file mode 100644 index 4995779c..00000000 --- a/packages/ui/src/viewer-diagram/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SchemaRelationshipDiagram as EntitiesDiagram } from './SchemaRelationshipDiagram'; From 2403aeaac72cc9c11d1571acda5147362eb505f6 Mon Sep 17 00:00:00 2001 From: Ethan Luc Date: Thu, 26 Mar 2026 10:38:31 -0400 Subject: [PATCH 43/43] fix: only render fixed height in docs mode --- .../Toolbar/FilterDropdowns.stories.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx index 0951bec9..e89b5ab1 100644 --- a/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx +++ b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx @@ -29,11 +29,16 @@ import { DictionaryTableViewer } from '../../../src/viewer-table/DictionaryTable import { withFilterDictionary, withMultipleDictionaries } from '../../dictionaryDecorator'; import themeDecorator from '../../themeDecorator'; -const withFixedHeight: Decorator = (Story) => ( -
- -
-); +const withFixedHeight: Decorator = (Story, context) => { + if (context.viewMode === 'docs') { + return ( +
+ +
+ ); + } + return ; +}; const meta = { component: DictionaryTableViewer,