Skip to content

Commit 1771ef8

Browse files
committed
Merge branch 'main' into feature/demo
2 parents 8823df1 + de90a79 commit 1771ef8

File tree

18 files changed

+621
-296
lines changed

18 files changed

+621
-296
lines changed

packages/client/src/rest/getDictionary.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import axios, { AxiosError } from 'axios';
2222
import { z as zod } from 'zod';
2323
import formatAxiosError from './formatAxiosError';
2424

25-
const dictionaryRecordSchema = DictionaryBase.extend({
25+
const dictionaryServerRecordSchema = DictionaryBase.extend({
2626
_id: zod.string(),
2727
createdAt: zod.string(),
28-
updatedAt: zod.string(),
2928
});
29+
export type DictionaryServerRecord = zod.infer<typeof dictionaryServerRecordSchema>;
3030

31-
const getDictionaryByNameAndVersionResponseSchema = zod.tuple([dictionaryRecordSchema]);
32-
const getDictionaryByIdResponseSchema = dictionaryRecordSchema;
31+
const getDictionaryByNameAndVersionResponseSchema = zod.tuple([dictionaryServerRecordSchema]);
32+
const getDictionaryByIdResponseSchema = dictionaryServerRecordSchema;
3333

3434
/**
3535
* Fetches a single Dictionary from a Lectern server. This can be done either by ID or by Name and Version.
@@ -47,7 +47,7 @@ export const getDictionary = async (
4747
options?: {
4848
showReferences?: boolean;
4949
},
50-
): Promise<Result<Dictionary>> => {
50+
): Promise<Result<DictionaryServerRecord>> => {
5151
const showReferences = options?.showReferences;
5252
try {
5353
if ('id' in dictionary) {

packages/ui/src/common/FieldBlock.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved
3+
*
4+
* This program and the accompanying materials are made available under the terms of
5+
* the GNU Affero General Public License v3.0. You should have received a copy of the
6+
* GNU Affero General Public License along with this program.
7+
* If not, see <http://www.gnu.org/licenses/>.
8+
*
9+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
10+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
11+
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
12+
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
13+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
14+
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
15+
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
16+
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
17+
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18+
*/
19+
20+
/** @jsxImportSource @emotion/react */
21+
22+
import { css } from '@emotion/react';
23+
import { CSSProperties, ReactNode } from 'react';
24+
25+
import { Theme } from '../theme';
26+
import { useThemeContext } from '../theme/ThemeContext';
27+
28+
export interface InlineCodeProps {
29+
children: ReactNode;
30+
style?: CSSProperties;
31+
}
32+
33+
const fieldBlockStyles = (theme: Theme) => css`
34+
${theme.typography.data}
35+
display: inline-flex;
36+
align-items: center;
37+
justify-content: center;
38+
gap: 4px;
39+
padding: 2px 5px;
40+
border-radius: 3px;
41+
background-color: #ececec;
42+
border: 0.5px solid black;
43+
transition: all 0.2s ease-in-out;
44+
width: fit-content;
45+
text-align: center;
46+
color: ${theme.colors.accent_dark};
47+
font-family: 'DM Mono', monospace;
48+
`;
49+
50+
const FieldBlock = ({ children, style }: InlineCodeProps) => {
51+
const theme: Theme = useThemeContext();
52+
return (
53+
<span css={fieldBlockStyles(theme)} style={style}>
54+
{children}
55+
</span>
56+
);
57+
};
58+
59+
export default FieldBlock;
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved
3+
*
4+
* This program and the accompanying materials are made available under the terms of
5+
* the GNU Affero General Public License v3.0. You should have received a copy of the
6+
* GNU Affero General Public License along with this program.
7+
* If not, see <http://www.gnu.org/licenses/>.
8+
*
9+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
10+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
11+
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
12+
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
13+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
14+
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
15+
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
16+
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
17+
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18+
*/
19+
import {
20+
RestrictionCondition,
21+
RestrictionRange,
22+
SchemaField,
23+
SchemaRestrictions,
24+
} from '@overture-stack/lectern-dictionary';
25+
import { CellContext } from '@tanstack/react-table';
26+
27+
export type RestrictionItem = {
28+
prefix: string[];
29+
content: string[];
30+
};
31+
32+
export type RestrictionField = RestrictionItem | undefined;
33+
34+
export type AllowedValuesBaseDisplayItem = {
35+
dependsOn?: RestrictionItem;
36+
regularExpression?: RestrictionItem;
37+
codeList?: RestrictionItem;
38+
range?: RestrictionItem;
39+
codeListWithCountRestrictions?: RestrictionItem;
40+
unique?: RestrictionItem;
41+
};
42+
43+
export type AllowedValuesColumnProps = {
44+
restrictions: CellContext<SchemaField, SchemaRestrictions>;
45+
};
46+
47+
//Helper functions
48+
const handleRange = (range: RestrictionRange): RestrictionItem | undefined => {
49+
const computeRestrictions = [
50+
{
51+
condition: range.min !== undefined && range.max !== undefined,
52+
prefix: ['Min:', 'Max:'],
53+
content: [`${range.min}`, `${range.max}`],
54+
},
55+
{
56+
condition: range.min !== undefined,
57+
prefix: 'Min:',
58+
content: `${range.min}`,
59+
},
60+
{
61+
condition: range.max !== undefined,
62+
prefix: 'Max:',
63+
content: `${range.max}`,
64+
},
65+
{
66+
condition: range.exclusiveMin !== undefined,
67+
prefix: 'Greater than:',
68+
content: `${range.exclusiveMin}`,
69+
},
70+
{
71+
condition: range.exclusiveMax !== undefined,
72+
prefix: 'Less than:',
73+
content: `${range.exclusiveMax}`,
74+
},
75+
];
76+
77+
const computedRestrictionItem = computeRestrictions.find((item) => item.condition);
78+
79+
return computedRestrictionItem ?
80+
{
81+
prefix:
82+
Array.isArray(computedRestrictionItem.prefix) ?
83+
[...computedRestrictionItem.prefix]
84+
: [computedRestrictionItem.prefix],
85+
content:
86+
Array.isArray(computedRestrictionItem.content) ?
87+
[...computedRestrictionItem.content]
88+
: [computedRestrictionItem.content],
89+
}
90+
: {
91+
prefix: [],
92+
content: [],
93+
};
94+
};
95+
96+
const handleCodeListsWithCountRestrictions = (
97+
codeList: string | string[] | number[],
98+
count: RestrictionRange,
99+
schemaField: SchemaField,
100+
): RestrictionItem | undefined => {
101+
const { isArray, delimiter } = schemaField;
102+
const delimiterText = isArray ? `, delimited by "${delimiter}"` : '';
103+
104+
const computeRestrictions = [
105+
{
106+
condition: typeof count == 'number',
107+
prefix: `Exactly ${count}${delimiterText} from:`,
108+
},
109+
{
110+
condition: count.min !== undefined && count.max !== undefined,
111+
prefix: `Select ${count.min} to ${count.max}${delimiterText} from:`,
112+
},
113+
{
114+
condition: count.min !== undefined,
115+
prefix: `At least ${count.min}${delimiterText} from:`,
116+
},
117+
{
118+
condition: count.max !== undefined,
119+
prefix: `Up to ${count.max}${delimiterText} from:`,
120+
},
121+
{
122+
condition: count.exclusiveMin !== undefined,
123+
prefix: `More than ${count.exclusiveMin}${delimiterText} from:`,
124+
},
125+
{
126+
condition: count.exclusiveMax !== undefined,
127+
prefix: `Fewer than ${count.exclusiveMax}${delimiterText} from:`,
128+
},
129+
];
130+
131+
const computedRestrictionItem = computeRestrictions.find((item) => item.condition);
132+
return computedRestrictionItem ?
133+
{
134+
prefix: [computedRestrictionItem.prefix],
135+
content: Array.isArray(codeList) ? codeList.map((item: string | number) => `${item}`) : [codeList],
136+
}
137+
: {
138+
prefix: [],
139+
content: [],
140+
};
141+
};
142+
143+
const handleDependsOn = (conditions: RestrictionCondition[]): RestrictionItem | undefined => {
144+
const allFields: string[] = conditions.flatMap((condition: RestrictionCondition) => {
145+
return condition.fields;
146+
});
147+
148+
return allFields.length > 0 ?
149+
{
150+
prefix: ['Depends on:'],
151+
content: [...new Set(allFields)],
152+
}
153+
: {
154+
prefix: [],
155+
content: [],
156+
};
157+
};
158+
159+
const handleRegularExpression = (regularExpression: string[] | string) => {
160+
const patterns = Array.isArray(regularExpression) ? regularExpression : [regularExpression];
161+
162+
return {
163+
prefix: Array.isArray(regularExpression) ? ['Must match patterns:'] : ['Must match pattern:'],
164+
content: patterns,
165+
};
166+
};
167+
168+
const handleCodeList = (codeList: string | string[] | number[]): RestrictionItem => {
169+
return {
170+
prefix: ['One of:'],
171+
content: Array.isArray(codeList) ? codeList.map((item: string | number) => `${item}`) : [codeList],
172+
};
173+
};
174+
175+
export const computeAllowedValuesColumn = (
176+
restrictions: SchemaRestrictions,
177+
schemaField: SchemaField,
178+
): AllowedValuesBaseDisplayItem => {
179+
const allowedValuesBaseDisplayItem: AllowedValuesBaseDisplayItem = {};
180+
181+
if (!restrictions || Object.keys(restrictions).length === 0) {
182+
return {};
183+
}
184+
185+
if ('if' in restrictions && restrictions.if !== undefined && restrictions.if.conditions !== undefined) {
186+
allowedValuesBaseDisplayItem.dependsOn = handleDependsOn(restrictions.if.conditions);
187+
}
188+
189+
if ('regex' in restrictions && restrictions.regex !== undefined) {
190+
allowedValuesBaseDisplayItem.regularExpression = handleRegularExpression(restrictions.regex);
191+
}
192+
193+
if ('codeList' in restrictions && restrictions.codeList !== undefined && !('count' in restrictions)) {
194+
allowedValuesBaseDisplayItem.codeList = handleCodeList(restrictions.codeList);
195+
}
196+
197+
if ('range' in restrictions && restrictions.range !== undefined) {
198+
allowedValuesBaseDisplayItem.range = handleRange(restrictions.range);
199+
}
200+
201+
if (
202+
'codeList' in restrictions &&
203+
restrictions.codeList !== undefined &&
204+
'count' in restrictions &&
205+
restrictions.count != undefined &&
206+
schemaField.isArray !== undefined
207+
) {
208+
allowedValuesBaseDisplayItem.codeListWithCountRestrictions = handleCodeListsWithCountRestrictions(
209+
restrictions.codeList,
210+
restrictions.count,
211+
schemaField,
212+
);
213+
}
214+
215+
if (schemaField.unique) {
216+
allowedValuesBaseDisplayItem.unique = { prefix: [], content: ['Must be unique'] };
217+
}
218+
219+
return allowedValuesBaseDisplayItem;
220+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright (c) 2025 The Ontario Institute for Cancer Research. All rights reserved
3+
*
4+
* This program and the accompanying materials are made available under the terms of
5+
* the GNU Affero General Public License v3.0. You should have received a copy of the
6+
* GNU Affero General Public License along with this program.
7+
* If not, see <http://www.gnu.org/licenses/>.
8+
*
9+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
10+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
11+
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
12+
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
13+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
14+
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
15+
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
16+
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
17+
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18+
*/
19+
20+
/** @jsxImportSource @emotion/react */
21+
22+
import { SchemaField, SchemaRestrictions } from '@overture-stack/lectern-dictionary';
23+
import FieldBlock from '../../../../../common/FieldBlock';
24+
import { computeAllowedValuesColumn } from './ComputeAllowedValues';
25+
26+
export const renderAllowedValuesColumn = (restrictions: SchemaRestrictions, schemaField: SchemaField) => {
27+
const restrictionItems = computeAllowedValuesColumn(restrictions, schemaField);
28+
29+
if (Object.keys(restrictionItems).length === 0) {
30+
return <strong>None</strong>;
31+
}
32+
33+
return (
34+
<>
35+
{Object.entries(restrictionItems).map(([key, value]) => {
36+
const { prefix, content } = value;
37+
38+
if (prefix.includes('Depends on:')) {
39+
return (
40+
<>
41+
<strong>{prefix}</strong>
42+
<br />
43+
{content.map((item, index) => (
44+
<FieldBlock key={index}>{item}</FieldBlock>
45+
))}
46+
</>
47+
);
48+
}
49+
50+
// For case of min and max
51+
if (prefix.length === content.length) {
52+
return (
53+
<>
54+
{prefix.map((prefix, index) => (
55+
<span key={index}>
56+
<strong>{prefix}</strong> {content[index]}{' '}
57+
</span>
58+
))}
59+
</>
60+
);
61+
}
62+
63+
return (
64+
<span>
65+
<strong>{prefix}</strong> <br />
66+
{content.join(',\n')}
67+
</span>
68+
);
69+
})}
70+
</>
71+
);
72+
};

packages/ui/src/viewer-table/DataTable/SchemaTable/Columns/Attribute.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import { css } from '@emotion/react';
2323
import { SchemaRestrictions } from '@overture-stack/lectern-dictionary';
24-
import React from 'react';
2524

2625
import Pill from '../../../../common/Pill';
2726
import OpenModalPill from '../OpenModalPill';

0 commit comments

Comments
 (0)