Skip to content

Commit f69bf54

Browse files
committed
Add JSON and URI datatypes #658 #1024
1 parent 7d114dc commit f69bf54

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1562
-269
lines changed

Cargo.lock

Lines changed: 35 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser/cli/src/DatatypeToTSTypeMap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ export const DatatypeToTSTypeMap = {
1111
[Datatype.STRING]: 'string',
1212
[Datatype.SLUG]: 'string',
1313
[Datatype.MARKDOWN]: 'string',
14+
[Datatype.URI]: 'string',
15+
[Datatype.JSON]: 'unknown',
1416
[Datatype.UNKNOWN]: 'JSONValue',
1517
};

browser/cli/src/PropertyRecord.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class PropertyRecord {
1313
]);
1414
}
1515

16-
public repordPropertyDefined(subject: string) {
16+
public reportPropertyDefined(subject: string) {
1717
this.knownProperties.add(subject);
1818

1919
if (this.missingProperties.has(subject)) {

browser/cli/src/generateOntology.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const generateOntology = async (
5252
const properties = dedupe(ontology.props.properties ?? []);
5353

5454
for (const prop of properties) {
55-
propertyRecord.repordPropertyDefined(prop);
55+
propertyRecord.reportPropertyDefined(prop);
5656
}
5757

5858
const [baseObjStr, reverseMapping] = await generateBaseObject(ontology);

browser/data-browser/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"@bugsnag/core": "^7.25.0",
99
"@bugsnag/js": "^7.25.0",
1010
"@bugsnag/plugin-react": "^7.25.0",
11+
"@codemirror/lang-json": "^6.0.2",
12+
"@codemirror/lint": "^6.8.5",
1113
"@dagrejs/dagre": "^1.1.4",
1214
"@dnd-kit/core": "^6.1.0",
1315
"@dnd-kit/sortable": "^8.0.0",
@@ -27,6 +29,9 @@
2729
"@tiptap/starter-kit": "^2.9.1",
2830
"@tiptap/suggestion": "^2.9.1",
2931
"@tomic/react": "workspace:*",
32+
"@uiw/codemirror-theme-github": "^4.24.1",
33+
"@uiw/react-codemirror": "^4.24.1",
34+
"clsx": "^2.1.1",
3035
"emoji-mart": "^5.6.0",
3136
"polished": "^4.3.1",
3237
"prismjs": "^1.29.0",
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import CodeMirror, {
2+
type BasicSetupOptions,
3+
type EditorView,
4+
} from '@uiw/react-codemirror';
5+
import { githubLight, githubDark } from '@uiw/codemirror-theme-github';
6+
import { json, jsonParseLinter } from '@codemirror/lang-json';
7+
import { linter, type Diagnostic } from '@codemirror/lint';
8+
import { useCallback, useMemo, useRef, useState } from 'react';
9+
import { styled, useTheme } from 'styled-components';
10+
11+
export interface JSONEditorProps {
12+
initialValue?: string;
13+
showErrorStyling?: boolean;
14+
required?: boolean;
15+
maxWidth?: string;
16+
onChange: (value: string) => void;
17+
onValidationChange?: (isValid: boolean) => void;
18+
onBlur?: () => void;
19+
}
20+
21+
const basicSetup: BasicSetupOptions = {
22+
lineNumbers: false,
23+
foldGutter: false,
24+
highlightActiveLine: true,
25+
indentOnInput: true,
26+
};
27+
28+
/**
29+
* ASYNC COMPONENT DO NOT IMPORT DIRECTLY, USE {@link JSONEditor.tsx}.
30+
*/
31+
const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
32+
initialValue,
33+
showErrorStyling,
34+
required,
35+
maxWidth,
36+
onChange,
37+
onValidationChange,
38+
onBlur,
39+
}) => {
40+
const theme = useTheme();
41+
const [value, setValue] = useState(initialValue ?? '');
42+
const latestDiagnostics = useRef<Diagnostic[]>([]);
43+
// We need to use callback because the compiler can't optimize the CodeMirror component.
44+
const handleChange = useCallback(
45+
(val: string) => {
46+
setValue(val);
47+
onChange(val);
48+
},
49+
[onChange],
50+
);
51+
52+
// Wrap jsonParseLinter so we can tap into diagnostics
53+
const validationLinter = useCallback(() => {
54+
const delegate = jsonParseLinter();
55+
56+
return (view: EditorView) => {
57+
const isEmpty = view.state.doc.length === 0;
58+
let diagnostics = delegate(view);
59+
60+
if (!required && isEmpty) {
61+
diagnostics = [];
62+
}
63+
64+
// Compare the diagnostics so we don't call the onValidationChange callback unnecessarily.
65+
const prev = latestDiagnostics.current;
66+
const changed =
67+
diagnostics.length !== prev.length ||
68+
diagnostics.some(
69+
(d, i) => d.from !== prev[i]?.from || d.message !== prev[i]?.message,
70+
);
71+
72+
if (changed) {
73+
latestDiagnostics.current = diagnostics;
74+
onValidationChange?.(diagnostics.length === 0);
75+
}
76+
77+
return diagnostics;
78+
};
79+
}, [onValidationChange]);
80+
81+
const extensions = useMemo(
82+
// eslint-disable-next-line react-compiler/react-compiler
83+
() => [json(), linter(validationLinter())],
84+
[validationLinter],
85+
);
86+
87+
return (
88+
<CodeEditorWrapper
89+
onBlur={() => onBlur?.()}
90+
className={showErrorStyling ? 'json-editor__error' : ''}
91+
>
92+
<CodeMirror
93+
value={value}
94+
onChange={handleChange}
95+
// We disable tab indenting because that would mess with accessibility/keyboard navigation.
96+
indentWithTab={false}
97+
theme={theme.darkMode ? githubDark : githubLight}
98+
minHeight='150px'
99+
maxHeight='40rem'
100+
maxWidth={maxWidth ?? '100%'}
101+
basicSetup={basicSetup}
102+
extensions={extensions}
103+
/>
104+
</CodeEditorWrapper>
105+
);
106+
};
107+
108+
export default AsyncJSONEditor;
109+
110+
const CodeEditorWrapper = styled.div`
111+
display: contents;
112+
113+
&.json-editor__error .cm-editor {
114+
border-color: ${p => p.theme.colors.alert} !important;
115+
}
116+
117+
& .cm-editor {
118+
border: 1px solid ${p => p.theme.colors.bg2};
119+
border-radius: ${p => p.theme.radius};
120+
/* padding: ${p => p.theme.size(2)}; */
121+
outline: none;
122+
123+
&:focus-within {
124+
border-color: ${p => p.theme.colors.main};
125+
}
126+
}
127+
`;

browser/data-browser/src/components/AtomicLink.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FaExternalLinkAlt } from 'react-icons/fa';
55
import { ErrorLook } from '../components/ErrorLook';
66
import { isRunningInTauri } from '../helpers/tauri';
77
import { useNavigateWithTransition } from '../hooks/useNavigateWithTransition';
8+
import clsx from 'clsx';
89

910
export interface AtomicLinkProps
1011
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
@@ -79,7 +80,7 @@ export const AtomicLink = forwardRef<HTMLAnchorElement, AtomicLinkProps>(
7980
return (
8081
<LinkView
8182
clean={clean}
82-
className={className}
83+
className={clsx(className, { 'atomic-link_external': href && !clean })}
8384
about={subject}
8485
onClick={handleClick}
8586
href={hrefConstructed}
@@ -92,7 +93,7 @@ export const AtomicLink = forwardRef<HTMLAnchorElement, AtomicLinkProps>(
9293
ref={ref}
9394
>
9495
{children}
95-
{href && !clean && <FaExternalLinkAlt />}
96+
{href && !clean && <FaExternalLinkAlt size='0.8em' />}
9697
</LinkView>
9798
);
9899
},
@@ -121,4 +122,10 @@ export const LinkView = styled.a<LinkViewProps>`
121122
&:active {
122123
color: ${props => props.theme.colors.mainDark};
123124
}
125+
126+
&.atomic-link_external {
127+
display: inline-flex;
128+
align-items: center;
129+
gap: 0.6ch;
130+
}
124131
`;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { lazy, Suspense } from 'react';
2+
import type { JSONEditorProps } from '../chunks/CodeEditor/AsyncJSONEditor';
3+
import { styled } from 'styled-components';
4+
5+
const AsyncJSONEditor = lazy(
6+
() => import('../chunks/CodeEditor/AsyncJSONEditor'),
7+
);
8+
9+
export const JSONEditor: React.FC<JSONEditorProps> = props => {
10+
return (
11+
<Suspense fallback={<Loader />}>
12+
<AsyncJSONEditor {...props} />
13+
</Suspense>
14+
);
15+
};
16+
17+
const Loader = styled.div`
18+
background-color: ${p => p.theme.colors.bg};
19+
border: 1px solid ${p => p.theme.colors.bg2};
20+
height: 150px;
21+
`;

browser/data-browser/src/components/PropVal.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ALL_PROPS_CONTAINER } from '../helpers/containers';
99
import { LoaderInline } from './Loader';
1010

1111
import type { JSX } from 'react';
12+
import { JSON_RENDERER_CLASS } from './datatypes/JSON';
1213

1314
type Props = {
1415
propertyURL: string;
@@ -82,6 +83,11 @@ export const PropValRow = styled.div<PropValRowProps>`
8283
grid-template-rows: auto 1fr;
8384
8485
@container ${ALL_PROPS_CONTAINER} (min-width: 500px) {
86+
&:has(.${JSON_RENDERER_CLASS}) {
87+
grid-template-columns: 1fr;
88+
gap: 0.5rem;
89+
}
90+
8591
grid-template-columns: 23ch auto;
8692
grid-template-rows: 1fr;
8793
}

browser/data-browser/src/components/SideBar/SideBarItem.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export interface SideBarItemProps {
55
}
66

77
/** SideBarItem should probably be wrapped in an AtomicLink for optimal behavior */
8-
// eslint-disable-next-line prettier/prettier
98
export const SideBarItem = styled('span')<SideBarItemProps>`
109
display: flex;
1110
min-height: ${props => props.theme.margin * 0.5 + 1}rem;

0 commit comments

Comments
 (0)