Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d729130
feat: add d3-dag library
ethanluc7 Feb 26, 2026
a113b30
feat: implement layout algorithm using d3-dag's sugiyama algorithm
ethanluc7 Feb 26, 2026
8defe14
feat: render nodes and edges from the generated layout
ethanluc7 Feb 26, 2026
4a3e5ec
feat: import new utils
ethanluc7 Feb 26, 2026
8ef038c
fix: adjust viewport configuration to support larger diagrams
ethanluc7 Feb 26, 2026
7ee353a
fix: re-add conditonal background and border logic
ethanluc7 Feb 26, 2026
dddb512
refactor: remove non-null assertion operator
ethanluc7 Mar 3, 2026
5e49787
refactor: remove type casting
ethanluc7 Mar 3, 2026
b15a9ec
refactor: centralize utility functions for use in other files
ethanluc7 Mar 4, 2026
ae312ed
refactor: update file with centralized logic for rendering content
ethanluc7 Mar 4, 2026
7518676
feat: move diagram view button to dictionary table viewer component
ethanluc7 Mar 4, 2026
8e6201e
feat: add diagram view link to allowed values
ethanluc7 Mar 4, 2026
892f7f1
feat: add separate grid algorithm for isFocused state
ethanluc7 Mar 4, 2026
95dbb7f
feat: update diagram subtitle to include logic for isFocused state
ethanluc7 Mar 4, 2026
5896b55
fix: update entity diagram relationship diagram story
ethanluc7 Mar 4, 2026
76e16e0
Merge branch 'main' into 398-table-view-interactions
ethanluc7 Mar 4, 2026
3672c99
Merge branch 'main' into 398-table-view-interactions
ethanluc7 Mar 5, 2026
335aff7
Merge branch '398-table-view-interactions' of https://github.com/over…
ethanluc7 Mar 5, 2026
f195b5b
refactor: remove duplicate function calls
ethanluc7 Mar 10, 2026
1eec9f7
fix: remove non null operator
ethanluc7 Mar 10, 2026
3875265
refactor: make code more readable
ethanluc7 Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/ui/src/utils/isFieldForeignKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
*
* 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 <http://www.gnu.org/licenses/>.
*
* 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 { Schema, SchemaField } from '@overture-stack/lectern-dictionary';

export function isFieldForeignKey(schema: Schema, field: SchemaField): boolean {
return (
schema.restrictions?.foreignKey?.some((fk) => fk.mappings.some((mapping) => mapping.local === field.name)) || false
);
}
26 changes: 26 additions & 0 deletions packages/ui/src/utils/isFieldUniqueKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
*
* 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 <http://www.gnu.org/licenses/>.
*
* 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 { Schema, SchemaField } from '@overture-stack/lectern-dictionary';

export function isFieldUniqueKey(schema: Schema, field: SchemaField): boolean {
return schema.restrictions?.uniqueKey?.includes(field.name) || field.unique === true || false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
/** @jsxImportSource @emotion/react */

import { css } from '@emotion/react';
import { SchemaField, SchemaFieldRestrictions, SchemaRestrictions } from '@overture-stack/lectern-dictionary';
import { Schema, SchemaField, SchemaFieldRestrictions, SchemaRestrictions } from '@overture-stack/lectern-dictionary';
import { type ReactNode } from 'react';

import ReadMoreText from '../../../../../common/ReadMoreText';
import { type Theme, useThemeContext } from '../../../../../theme/index';
import { isFieldForeignKey } from '../../../../../utils/isFieldForeignKey';
import { useDiagramViewContext } from '../../../../DiagramViewContext';

import { computeAllowedValuesColumn, type RestrictionItem } from './ComputeAllowedValues';

Expand All @@ -47,6 +49,22 @@ const codeListContentStyle = css`
gap: 2px;
`;

const viewInDiagramButtonStyle = (theme: Theme) => css`
${theme.typography.paragraphSmallBold}
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0;
background: none;
border: none;
text-decoration: underline;
cursor: pointer;
margin-top: 4px;
&:hover {
color: ${theme.colors.secondary};
}
`;

const renderRestrictionItem = (value: RestrictionItem, key: string): ReactNode => {
const { prefix, content } = value;
return (
Expand All @@ -71,9 +89,12 @@ export const renderAllowedValuesColumn = (
fieldLevelRestrictions: SchemaFieldRestrictions,
schemaLevelRestrictions: SchemaRestrictions,
currentSchemaField: SchemaField,
schema?: Schema,
) => {
const items = computeAllowedValuesColumn(fieldLevelRestrictions, schemaLevelRestrictions, currentSchemaField);
const theme: Theme = useThemeContext();
const { openFocusedDiagram } = useDiagramViewContext();
const isForeignKey = schema ? isFieldForeignKey(schema, currentSchemaField) : false;

if (!items || Object.keys(items).length === 0) {
return (
Expand All @@ -83,6 +104,14 @@ export const renderAllowedValuesColumn = (
`}
>
No restrictions provided for this field.
{isForeignKey && schema && (
<button
css={viewInDiagramButtonStyle(theme)}
onClick={() => openFocusedDiagram({ schemaName: schema.name, fieldName: currentSchemaField.name })}
>
View in diagram
</button>
)}
</span>
);
}
Expand All @@ -98,6 +127,14 @@ export const renderAllowedValuesColumn = (
: null
);
})}
{isForeignKey && schema && (
<button
css={viewInDiagramButtonStyle(theme)}
onClick={() => openFocusedDiagram({ schemaName: schema.name, fieldName: currentSchemaField.name })}
>
View in diagram
</button>
)}
</ReadMoreText>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@
/** @jsxImportSource @emotion/react */

import { css } from '@emotion/react';
import type { SchemaField } from '@overture-stack/lectern-dictionary';
import type { Schema, SchemaField } from '@overture-stack/lectern-dictionary';
import { SchemaFieldRestrictions } from '@overture-stack/lectern-dictionary';
import { useState } from 'react';

import { type Theme, useThemeContext } from '../../../../theme/index';
import Key from '../../../../theme/icons/Key';
import Eye from '../../../../theme/icons/Eye';
import { isFieldForeignKey } from '../../../../utils/isFieldForeignKey';
import { isFieldRequired } from '../../../../utils/isFieldRequired';
import { isFieldUniqueKey } from '../../../../utils/isFieldUniqueKey';
import { ConditionalLogicModal } from '../../../ConditionalLogicModal/ConditionalLogicModal';
import { NoMarginParagraph } from '../../../../theme/emotion';
import OpenModalButton from '../../../OpenModalButton';
import { useDiagramViewContext } from '../../../DiagramViewContext';

export type Attributes = 'Required' | 'Optional' | 'Required When';

Expand All @@ -41,10 +46,37 @@ const containerStyle = (theme: Theme) => css`
${theme.typography.paragraphSmallBold}
`;

const buttonTextContainer = css`
const diagramLinkStyle = (theme: Theme) => css`
${theme.typography.paragraphSmallBold}
padding: 0;
background: none;
border: none;
color: ${theme.colors.black};
text-decoration: underline;
cursor: pointer;
&:hover {
color: ${theme.colors.secondary};
}
`;

const hoverGroupStyle = (theme: Theme) => css`
display: flex;
flex-direction: column;
gap: 0;
align-items: center;
gap: 10px;
&:has(button:hover) svg {
stroke: ${theme.colors.secondary};
}
&:has(button:hover) button {
color: ${theme.colors.secondary};
}
`;

const iconGroupStyle = css`
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
`;

/**
Expand All @@ -55,21 +87,30 @@ const buttonTextContainer = css`
export const renderAttributesColumn = (
schemaFieldRestrictions: SchemaFieldRestrictions,
currentSchemaField?: SchemaField,
schema?: Schema,
) => {
const theme: Theme = useThemeContext();
const [isOpen, setIsOpen] = useState(false);
const { openFocusedDiagram } = useDiagramViewContext();
const [isOpen, setIsOpen] = useState<boolean>(false);
const showConditional = !!(schemaFieldRestrictions && 'if' in schemaFieldRestrictions);
const isUniqueKey = schema && currentSchemaField && isFieldUniqueKey(schema, currentSchemaField);
const isForeignKey = schema && currentSchemaField && isFieldForeignKey(schema, currentSchemaField);
const isRequired = currentSchemaField && isFieldRequired(currentSchemaField);

return (
<div css={containerStyle(theme)}>
{showConditional ?
<>
<OpenModalButton onClick={() => setIsOpen(true)}>
<div css={buttonTextContainer}>
<div css={hoverGroupStyle(theme)}>
<div css={iconGroupStyle}>
<Eye width={24} height={24} />
{(isUniqueKey || isForeignKey) && <Key width={18} height={18} />}
</div>
<OpenModalButton onClick={() => setIsOpen(true)}>
<p css={NoMarginParagraph}>Required</p>
<p css={NoMarginParagraph}>When</p>
</div>
</OpenModalButton>
</OpenModalButton>
</div>
{currentSchemaField && (
<ConditionalLogicModal
isOpen={isOpen}
Expand All @@ -79,7 +120,26 @@ export const renderAttributesColumn = (
/>
)}
</>
: <div>{currentSchemaField && isFieldRequired(currentSchemaField) ? 'Required' : 'Optional'}</div>}
: isForeignKey ?
<div css={hoverGroupStyle(theme)}>
<Key width={18} height={18} />
<button
css={diagramLinkStyle(theme)}
onClick={() => {
if (!schema || !currentSchemaField) {
return;
}
openFocusedDiagram({ schemaName: schema.name, fieldName: currentSchemaField.name });
}}
>
{isRequired ? 'Required' : 'Optional'}
</button>
</div>
: <>
{isUniqueKey && <Key width={18} height={18} />}
<div>{isRequired ? 'Required' : 'Optional'}</div>
</>
}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const getSchemaBaseColumns = (schema: Schema) => [
cell: (attribute: CellContext<SchemaField, unknown>) => {
const schemaField: SchemaField = attribute.row.original;
const fieldLevelRestrictions: SchemaFieldRestrictions = schemaField.restrictions;
return renderAttributesColumn(fieldLevelRestrictions, schemaField);
return renderAttributesColumn(fieldLevelRestrictions, schemaField, schema);
},
}),

Expand All @@ -70,7 +70,7 @@ export const getSchemaBaseColumns = (schema: Schema) => [
const fieldLevelRestrictions = schemaField.restrictions;
const schemaLevelRestrictions = schema.restrictions;

return renderAllowedValuesColumn(fieldLevelRestrictions, schemaLevelRestrictions, schemaField);
return renderAllowedValuesColumn(fieldLevelRestrictions, schemaLevelRestrictions, schemaField, schema);
},
}),
];
67 changes: 67 additions & 0 deletions packages/ui/src/viewer-table/DiagramViewContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
*
* 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 <http://www.gnu.org/licenses/>.
*
* 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 { createContext, useCallback, useContext, useState } from 'react';
import type { ReactNode } from 'react';

type DiagramViewContextType = {
isOpen: boolean;
focusField: { schemaName: string; fieldName: string } | undefined;
openDiagram: () => void;
openFocusedDiagram: (field: { schemaName: string; fieldName: string }) => void;
closeDiagram: () => void;
};

export const DiagramViewContext = createContext<DiagramViewContextType>({
isOpen: false,
focusField: undefined,
openDiagram: () => {},
openFocusedDiagram: () => {},
closeDiagram: () => {},
});

export const useDiagramViewContext = () => useContext(DiagramViewContext);

export function DiagramViewProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const [focusField, setFocusField] = useState<{ schemaName: string; fieldName: string } | undefined>(undefined);

const openDiagram = useCallback(() => {
setFocusField(undefined);
setIsOpen(true);
}, []);

const openFocusedDiagram = useCallback((field: { schemaName: string; fieldName: string }) => {
setFocusField(field);
setIsOpen(true);
}, []);

const closeDiagram = useCallback(() => {
setIsOpen(false);
setFocusField(undefined);
}, []);

return (
<DiagramViewContext.Provider value={{ isOpen, focusField, openDiagram, openFocusedDiagram, closeDiagram }}>
{children}
</DiagramViewContext.Provider>
);
}
Loading