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';
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/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
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' };
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/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' },
- ],
- },
-};
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' },
- },
-};
diff --git a/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx b/packages/ui/stories/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.stories.tsx
similarity index 67%
rename from packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx
rename to packages/ui/stories/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.stories.tsx
index 56348a6f..e063ca03 100644
--- a/packages/ui/stories/viewer-table/EntityRelationshipDiagram.stories.tsx
+++ b/packages/ui/stories/viewer-table/EntityRelationshipDiagram/EntityRelationshipDiagram.stories.tsx
@@ -28,27 +28,38 @@ 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';
+} 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',
+ 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;
@@ -64,6 +75,9 @@ const StoryWrapper = ({ dictionary }: { dictionary: Dictionary }) => {
);
};
+/**
+ * Full PCGL dictionary with 17 schemas and multiple foreign key relationships.
+ */
export const Default: Story = {
args: {
dictionary: DictionarySample as Dictionary,
@@ -75,6 +89,9 @@ export const Default: Story = {
),
};
+/**
+ * A simplified clinical data model with a handful of related schemas.
+ */
export const SimpleClinicalExample: Story = {
args: {
dictionary: SimpleClinicalERDiagram as Dictionary,
@@ -86,6 +103,9 @@ export const SimpleClinicalExample: Story = {
),
};
+/**
+ * A dictionary with only one schema and no relationships.
+ */
export const SingleSchema: Story = {
args: {
dictionary: SingleSchemaFixture as Dictionary,
@@ -97,6 +117,9 @@ export const SingleSchema: Story = {
),
};
+/**
+ * Two schemas with no foreign key relationships between them.
+ */
export const TwoIsolatedSchemas: Story = {
args: {
dictionary: TwoIsolatedSchemasFixture as Dictionary,
@@ -108,6 +131,9 @@ export const TwoIsolatedSchemas: Story = {
),
};
+/**
+ * Two schemas connected by a single foreign key.
+ */
export const TwoSchemaLinear: Story = {
args: {
dictionary: TwoSchemaLinearFixture as Dictionary,
@@ -119,6 +145,9 @@ export const TwoSchemaLinear: Story = {
),
};
+/**
+ * Three schemas linked in a chain: A → B → C.
+ */
export const ThreeSchemaChain: Story = {
args: {
dictionary: ThreeSchemaChainFixture as Dictionary,
@@ -130,6 +159,9 @@ export const ThreeSchemaChain: Story = {
),
};
+/**
+ * A schema with multiple foreign keys referencing different schemas.
+ */
export const MultiFk: Story = {
args: {
dictionary: MultiFkFixture as Dictionary,
@@ -141,6 +173,9 @@ export const MultiFk: Story = {
),
};
+/**
+ * One parent schema referenced by multiple child schemas (fan-out pattern).
+ */
export const FanOut: Story = {
args: {
dictionary: FanOutFixture as Dictionary,
@@ -152,6 +187,9 @@ export const FanOut: Story = {
),
};
+/**
+ * A mix of linear chains, fan-out, and isolated schemas.
+ */
export const MixedRelations: Story = {
args: {
dictionary: MixedRelationsFixture as Dictionary,
@@ -163,6 +201,9 @@ export const MixedRelations: Story = {
),
};
+/**
+ * A foreign key mapping that uses a compound (multi-field) key.
+ */
export const CompoundKey: Story = {
args: {
dictionary: CompoundKeyFixture as Dictionary,
@@ -174,6 +215,9 @@ export const CompoundKey: Story = {
),
};
+/**
+ * Schemas with cyclical foreign key references (A → B → A).
+ */
export const Cyclical: Story = {
args: {
dictionary: CyclicalFixture as Dictionary,
@@ -185,6 +229,10 @@ export const Cyclical: Story = {
),
};
+/**
+ * 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,
@@ -194,4 +242,4 @@ export const NonUniqueForeignKey: Story = {
),
-};
\ No newline at end of file
+};
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) => ,
+};
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/FilterDropdowns.stories.tsx b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx
new file mode 100644
index 00000000..e89b5ab1
--- /dev/null
+++ b/packages/ui/stories/viewer-table/Toolbar/FilterDropdowns.stories.tsx
@@ -0,0 +1,157 @@
+/*
+ *
+ * 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 { Decorator, 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 withFixedHeight: Decorator = (Story, context) => {
+ if (context.viewMode === 'docs') {
+ return (
+
+
+
+ );
+ }
+ return ;
+};
+
+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.',
+ },
+ story: {
+ autoplay: false,
+ inline: false,
+ iframeHeight: 600,
+ },
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+/**
+ * Default view with two filter categories configured.
+ */
+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' }],
+ },
+};
+
+/**
+ * Selects Submitter: "Clinician" to show only matching schemas.
+ * Navigate to the canvas tab to see a live demo.
+ */
+export const FilterSelected: Story = {
+ decorators: [withFixedHeight, withFilterDictionary],
+ args: {
+ filterDropdowns: [
+ { label: 'Submitter', filterProperty: 'meta.submitter' },
+ { 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' });
+ await userEvent.click(filtersButton);
+ const clinicianCheckbox = await canvas.findByLabelText('Clinician');
+ await userEvent.click(clinicianCheckbox);
+ },
+};
+
+/**
+ * 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: [withFixedHeight, withFilterDictionary],
+ args: {
+ filterDropdowns: [
+ { label: 'Submitter', filterProperty: 'meta.submitter' },
+ { 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' });
+ 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.
+ */
+export const NoConfig: Story = {
+ decorators: [withMultipleDictionaries],
+};
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;