Skip to content

Commit 349acf8

Browse files
committed
Add JSON and URI fields to tables #658 #1024
1 parent f69bf54 commit 349acf8

File tree

18 files changed

+344
-37
lines changed

18 files changed

+344
-37
lines changed

browser/data-browser/src/chunks/CodeEditor/AsyncJSONEditor.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface JSONEditorProps {
1313
showErrorStyling?: boolean;
1414
required?: boolean;
1515
maxWidth?: string;
16+
autoFocus?: boolean;
1617
onChange: (value: string) => void;
1718
onValidationChange?: (isValid: boolean) => void;
1819
onBlur?: () => void;
@@ -33,6 +34,7 @@ const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
3334
showErrorStyling,
3435
required,
3536
maxWidth,
37+
autoFocus,
3638
onChange,
3739
onValidationChange,
3840
onBlur,
@@ -90,6 +92,7 @@ const AsyncJSONEditor: React.FC<JSONEditorProps> = ({
9092
className={showErrorStyling ? 'json-editor__error' : ''}
9193
>
9294
<CodeMirror
95+
autoFocus={autoFocus}
9396
value={value}
9497
onChange={handleChange}
9598
// We disable tab indenting because that would mess with accessibility/keyboard navigation.

browser/data-browser/src/chunks/MarkdownEditor/AsyncMarkdownEditor.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { transition } from '../../helpers/transition';
1818
export type AsyncMarkdownEditorProps = {
1919
placeholder?: string;
2020
initialContent?: string;
21+
autoFocus?: boolean;
2122
onChange?: (content: string) => void;
2223
id?: string;
2324
labelId?: string;
@@ -31,6 +32,7 @@ const LINE_HEIGHT = 1.15;
3132
export default function AsyncMarkdownEditor({
3233
placeholder,
3334
initialContent,
35+
autoFocus,
3436
id,
3537
labelId,
3638
onChange,
@@ -76,6 +78,7 @@ export default function AsyncMarkdownEditor({
7678
extensions,
7779
content: markdown,
7880
onBlur,
81+
autofocus: autoFocus,
7982
editorProps: {
8083
attributes: {
8184
...(id && { id }),

browser/data-browser/src/components/Dialog/index.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ export const VAR_DIALOG_INNER_WIDTH = '--dialog-inner-width';
3838
const ANIM_MS = 80;
3939
const ANIM_SPEED = `${ANIM_MS}ms`;
4040

41-
interface DialogSlotProps {
42-
className?: string;
43-
}
44-
45-
type DialogSlotComponent = React.FC<React.PropsWithChildren<DialogSlotProps>>;
41+
type DialogSlotComponent = React.FC<
42+
React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>
43+
>;
4644

4745
/**
4846
* Component to build a dialog. The content of this component are rendered in a
@@ -179,18 +177,17 @@ const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
179177
);
180178
};
181179

182-
export const DialogTitle: DialogSlotComponent = ({ children, className }) => (
183-
<Slot slot={DialogSlot.Title} as='header' className={className}>
180+
export const DialogTitle: DialogSlotComponent = ({ children, ...props }) => (
181+
<Slot slot={DialogSlot.Title} as='header' {...props}>
184182
{children}
185183
</Slot>
186184
);
187185

188186
/**
189-
* Dialog section that is scrollable. Put your main content here. Should be no
190-
* larger than 4rem
187+
* Dialog section that is scrollable. Put your main content here.
191188
*/
192-
export const DialogContent: DialogSlotComponent = ({ children, className }) => (
193-
<DialogContentSlot slot={DialogSlot.Content} as='main' className={className}>
189+
export const DialogContent: DialogSlotComponent = ({ children, ...props }) => (
190+
<DialogContentSlot slot={DialogSlot.Content} as='main' {...props}>
194191
{children}
195192
</DialogContentSlot>
196193
);
@@ -199,16 +196,16 @@ export const DialogContent: DialogSlotComponent = ({ children, className }) => (
199196
* Bottom part of the Dialog that is always visible. Place your buttons here.
200197
* Should be no larger than 4rem
201198
*/
202-
export const DialogActions: DialogSlotComponent = ({ children, className }) => (
203-
<DialogActionsSlot
204-
slot={DialogSlot.Actions}
205-
as='footer'
206-
className={className}
207-
>
199+
export const DialogActions: DialogSlotComponent = ({ children, ...props }) => (
200+
<DialogActionsSlot slot={DialogSlot.Actions} as='footer' {...props}>
208201
{children}
209202
</DialogActionsSlot>
210203
);
211204

205+
Dialog.Title = DialogTitle;
206+
Dialog.Content = DialogContent;
207+
Dialog.Actions = DialogActions;
208+
212209
const CloseButtonSlot = styled(Slot)`
213210
justify-self: end;
214211
`;

browser/data-browser/src/components/TableEditor/Cell.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ export function Cell({
102102

103103
const handleMouseDown = useCallback(
104104
(e: React.MouseEvent<HTMLDivElement>) => {
105+
if (disabledKeyboardInteractions.has(KeyboardInteraction.ExitEditMode)) {
106+
return;
107+
}
108+
105109
setMouseDown(true);
106110

107111
// When Shift is pressed, enter multi-select mode
@@ -126,10 +130,6 @@ export function Cell({
126130
return;
127131
}
128132

129-
if (disabledKeyboardInteractions.has(KeyboardInteraction.ExitEditMode)) {
130-
return;
131-
}
132-
133133
if (isActive && cursorMode === CursorMode.Edit) {
134134
return;
135135
}
@@ -148,14 +148,18 @@ export function Cell({
148148
);
149149

150150
const handleClick = useCallback(() => {
151+
if (disabledKeyboardInteractions.has(KeyboardInteraction.ExitEditMode)) {
152+
return;
153+
}
154+
151155
if (markEnterEditMode) {
152156
setMultiSelectCorner(undefined, undefined);
153157
setMouseDown(false);
154158

155159
setCursorMode(CursorMode.Edit);
156160
setMarkEnterEditMode(false);
157161
}
158-
}, [markEnterEditMode]);
162+
}, [markEnterEditMode, disabledKeyboardInteractions]);
159163

160164
useLayoutEffect(() => {
161165
if (!ref.current) {

browser/data-browser/src/components/forms/InputJSON.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import {
88
} from './formValidation/useValidation';
99
import { JSONEditor } from '../JSONEditor';
1010
import { JSON_RENDERER_CLASS } from '../datatypes/JSON';
11-
import { CSSVar } from '../../helpers/CSSVar';
12-
13-
const JSON_EDITOR_MAX_WIDTH = new CSSVar('json-editor-max-width');
1411

1512
export const InputJSON: React.FC<InputProps> = ({
1613
resource,
1714
property,
1815
commit,
1916
commitDebounceInterval,
17+
autoFocus,
2018
...props
2119
}) => {
2220
const [value, setValue] = useValue(resource, property.subject, {
@@ -52,7 +50,7 @@ export const InputJSON: React.FC<InputProps> = ({
5250
<Wrapper className={JSON_RENDERER_CLASS}>
5351
<JSONEditor
5452
initialValue={initialValue}
55-
maxWidth={JSON_EDITOR_MAX_WIDTH.var()}
53+
autoFocus={autoFocus}
5654
onChange={handleUpdate}
5755
onBlur={setTouched}
5856
showErrorStyling={!!error}
@@ -66,7 +64,6 @@ export const InputJSON: React.FC<InputProps> = ({
6664
};
6765

6866
const Wrapper = styled.div`
69-
${JSON_EDITOR_MAX_WIDTH.define(p => `calc(100cqw - ${p.theme.size()})`)}
7067
flex: 1;
7168
position: relative;
7269
`;

browser/data-browser/src/helpers/iconMap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
FaListUl,
3232
FaMarkdown,
3333
FaRegSquareCheck,
34+
FaLink,
35+
FaCode,
3436
} from 'react-icons/fa6';
3537

3638
const iconMap = new Map<string, IconType>([
@@ -70,4 +72,6 @@ export const dataTypeIconMap = new Map<string, IconType>([
7072
[Datatype.BOOLEAN, FaRegSquareCheck],
7173
[Datatype.DATE, FaCalendar],
7274
[Datatype.TIMESTAMP, FaClock],
75+
[Datatype.URI, FaLink],
76+
[Datatype.JSON, FaCode],
7377
]);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { JSONValue, useProperty } from '@tomic/react';
2+
3+
import { CellContainer, DisplayCellProps, EditCellProps } from './Type';
4+
5+
import { useMemo, type JSX } from 'react';
6+
import styled from 'styled-components';
7+
import { IconButton } from '../../../components/IconButton/IconButton';
8+
import { FaPencil } from 'react-icons/fa6';
9+
import { Dialog, useDialog } from '../../../components/Dialog';
10+
import {
11+
KeyboardInteraction,
12+
useCellOptions,
13+
} from '../../../components/TableEditor';
14+
import { addIf } from '../../../helpers/addIf';
15+
import { useTableEditorContext } from '../../../components/TableEditor/TableEditorContext';
16+
import { InputJSON } from '../../../components/forms/InputJSON';
17+
18+
function JSONCellEdit({
19+
value,
20+
property,
21+
resource,
22+
}: EditCellProps<JSONValue>): JSX.Element {
23+
const [dialogProps, show, close, isOpen] = useDialog({
24+
onSuccess: () => {
25+
tableRef.current?.focus();
26+
},
27+
onCancel: () => {
28+
tableRef.current?.focus();
29+
},
30+
});
31+
const prop = useProperty(property);
32+
33+
const { tableRef } = useTableEditorContext();
34+
35+
const options = useMemo(
36+
() => ({
37+
disabledKeyboardInteractions: new Set([
38+
...addIf(
39+
isOpen,
40+
KeyboardInteraction.ExitEditMode,
41+
KeyboardInteraction.EditNextRow,
42+
),
43+
]),
44+
}),
45+
[isOpen],
46+
);
47+
48+
useCellOptions(options);
49+
50+
const openDialog = () => {
51+
show();
52+
};
53+
54+
const displayValue = JSON.stringify(value);
55+
56+
return (
57+
<>
58+
<IconButton title='Open edit dialog' onClick={openDialog} autoFocus>
59+
<FaPencil />
60+
</IconButton>
61+
<div>{displayValue}</div>
62+
<Dialog {...dialogProps} width='70ch'>
63+
{isOpen && (
64+
<>
65+
<Dialog.Title>
66+
<h1>Edit {prop.shortname}</h1>
67+
</Dialog.Title>
68+
<StyledDialogContent
69+
onKeyDown={e => {
70+
if (e.key === 'Escape') {
71+
e.preventDefault();
72+
close(true);
73+
}
74+
}}
75+
>
76+
<InputJSON commit autoFocus resource={resource} property={prop} />
77+
</StyledDialogContent>
78+
</>
79+
)}
80+
</Dialog>
81+
</>
82+
);
83+
}
84+
85+
function JSONCellDisplay({ value }: DisplayCellProps<JSONValue>): JSX.Element {
86+
const displayValue = JSON.stringify(value);
87+
88+
return <>{displayValue}</>;
89+
}
90+
91+
export const JSONCell: CellContainer<JSONValue> = {
92+
Edit: JSONCellEdit,
93+
Display: JSONCellDisplay,
94+
};
95+
96+
const StyledDialogContent = styled(Dialog.Content)`
97+
padding-top: 2px;
98+
`;
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { JSONValue, useProperty } from '@tomic/react';
2+
3+
import { CellContainer, DisplayCellProps, EditCellProps } from './Type';
4+
5+
import { useMemo, type JSX } from 'react';
6+
import styled from 'styled-components';
7+
import { IconButton } from '../../../components/IconButton/IconButton';
8+
import { FaPencil } from 'react-icons/fa6';
9+
import { Dialog, useDialog } from '../../../components/Dialog';
10+
import {
11+
KeyboardInteraction,
12+
useCellOptions,
13+
} from '../../../components/TableEditor';
14+
import { addIf } from '../../../helpers/addIf';
15+
import InputMarkdown from '../../../components/forms/InputMarkdown';
16+
import { useTableEditorContext } from '../../../components/TableEditor/TableEditorContext';
17+
18+
function MarkdownCellEdit({
19+
value,
20+
property,
21+
resource,
22+
}: EditCellProps<JSONValue>): JSX.Element {
23+
const [dialogProps, show, _close, isOpen] = useDialog({
24+
onSuccess: () => {
25+
tableRef.current?.focus();
26+
},
27+
onCancel: () => {
28+
tableRef.current?.focus();
29+
},
30+
});
31+
const prop = useProperty(property);
32+
33+
const { tableRef } = useTableEditorContext();
34+
35+
const options = useMemo(
36+
() => ({
37+
disabledKeyboardInteractions: new Set([
38+
...addIf(
39+
isOpen,
40+
KeyboardInteraction.ExitEditMode,
41+
KeyboardInteraction.EditNextRow,
42+
),
43+
]),
44+
}),
45+
[isOpen],
46+
);
47+
48+
useCellOptions(options);
49+
50+
const openDialog = () => {
51+
show();
52+
};
53+
54+
return (
55+
<>
56+
<IconButton title='Open edit dialog' onClick={openDialog} autoFocus>
57+
<FaPencil />
58+
</IconButton>
59+
<div>{value as string}</div>
60+
<Dialog {...dialogProps} width='70ch'>
61+
{isOpen && (
62+
<>
63+
<Dialog.Title>
64+
<h1>Edit {prop.shortname}</h1>
65+
</Dialog.Title>
66+
<StyledDialogContent>
67+
<InputMarkdown
68+
autoFocus
69+
commit
70+
resource={resource}
71+
property={prop}
72+
/>
73+
</StyledDialogContent>
74+
</>
75+
)}
76+
</Dialog>
77+
</>
78+
);
79+
}
80+
81+
function MarkdownCellDisplay({
82+
value,
83+
}: DisplayCellProps<JSONValue>): JSX.Element {
84+
return <>{value}</>;
85+
}
86+
87+
export const MarkdownCell: CellContainer<JSONValue> = {
88+
Edit: MarkdownCellEdit,
89+
Display: MarkdownCellDisplay,
90+
};
91+
92+
const StyledDialogContent = styled(Dialog.Content)`
93+
padding-top: 2px;
94+
`;

0 commit comments

Comments
 (0)