@dxos/react-ui-form 0.8.4-main.5ad4a44 → 0.8.4-main.66e292d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +223 -205
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +223 -205
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +5 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +5 -5
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +5 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +47 -33
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +3 -3
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +2 -2
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +1 -0
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +5 -5
- package/dist/types/src/components/Form/factory.d.ts +2 -2
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +6 -5
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +11 -9
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +8 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +4 -4
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +1 -0
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +5 -5
- package/dist/types/src/util/field.d.ts +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -31
- package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.tsx +3 -3
- package/src/components/Form/ArrayField.tsx +3 -1
- package/src/components/Form/Defaults.tsx +1 -1
- package/src/components/Form/Form.stories.tsx +24 -24
- package/src/components/Form/Form.tsx +8 -5
- package/src/components/Form/FormActions.tsx +7 -2
- package/src/components/Form/FormContent.tsx +53 -13
- package/src/components/Form/FormContext.tsx +4 -4
- package/src/components/Form/Input.tsx +2 -2
- package/src/components/Form/RefField.tsx +16 -2
- package/src/components/Form/custom/SelectOptionsInput.tsx +1 -1
- package/src/components/Form/factory.tsx +7 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +25 -25
- package/src/components/ObjectPicker/ObjectPicker.tsx +14 -14
- package/src/components/ViewEditor/ViewEditor.stories.tsx +46 -32
- package/src/components/ViewEditor/ViewEditor.test.tsx +4 -3
- package/src/components/ViewEditor/ViewEditor.tsx +41 -97
- package/src/hooks/useForm.ts +8 -7
- package/src/hooks/useQueryRefOptions.ts +6 -3
- package/src/hooks/useRefQueryLookupHandler.ts +5 -5
- package/src/translations.ts +5 -5
- package/src/util/field.ts +25 -25
- package/src/util/formatting.ts +22 -22
- package/src/util/refs.ts +6 -6
|
@@ -6,32 +6,23 @@ import * as Array from 'effect/Array';
|
|
|
6
6
|
import * as Match from 'effect/Match';
|
|
7
7
|
import * as Option from 'effect/Option';
|
|
8
8
|
import * as Schema from 'effect/Schema';
|
|
9
|
-
import React, { forwardRef, useCallback,
|
|
9
|
+
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useState } from 'react';
|
|
10
10
|
|
|
11
|
+
import { QueryAST } from '@dxos/echo';
|
|
11
12
|
import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
12
|
-
import { type SchemaRegistry } from '@dxos/echo-db';
|
|
13
|
+
import { Filter, Query, type SchemaRegistry } from '@dxos/echo-db';
|
|
13
14
|
import { invariant } from '@dxos/invariant';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
IconButton,
|
|
17
|
-
Input,
|
|
18
|
-
type ThemedClassName,
|
|
19
|
-
useDensityContext,
|
|
20
|
-
useElevationContext,
|
|
21
|
-
useThemeContext,
|
|
22
|
-
useTranslation,
|
|
23
|
-
} from '@dxos/react-ui';
|
|
24
|
-
import { Editor, createBasicExtensions, createThemeExtensions } from '@dxos/react-ui-editor';
|
|
15
|
+
import { Callout, IconButton, Input, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
16
|
+
import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
|
|
25
17
|
import { List } from '@dxos/react-ui-list';
|
|
26
18
|
import { cardSpacing } from '@dxos/react-ui-stack';
|
|
27
|
-
import { inputTheme } from '@dxos/react-ui-theme';
|
|
28
19
|
import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
|
|
29
20
|
import {
|
|
30
|
-
type DataType,
|
|
31
21
|
FieldSchema,
|
|
32
22
|
type FieldType,
|
|
33
23
|
ProjectionModel,
|
|
34
24
|
VIEW_FIELD_LIMIT,
|
|
25
|
+
type View,
|
|
35
26
|
getTypenameFromQuery,
|
|
36
27
|
} from '@dxos/schema';
|
|
37
28
|
|
|
@@ -45,14 +36,15 @@ const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
|
45
36
|
export type ViewEditorProps = ThemedClassName<
|
|
46
37
|
{
|
|
47
38
|
schema: Schema.Schema.AnyNoContext;
|
|
48
|
-
view:
|
|
49
|
-
mode?: 'schema' | '
|
|
39
|
+
view: View.View;
|
|
40
|
+
mode?: 'schema' | 'tag';
|
|
50
41
|
registry?: SchemaRegistry;
|
|
51
42
|
readonly?: boolean;
|
|
52
43
|
showHeading?: boolean;
|
|
53
|
-
onQueryChanged?: (query:
|
|
44
|
+
onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
|
|
54
45
|
onDelete?: (fieldId: string) => void;
|
|
55
|
-
} & Pick<FormProps<any>, 'outerSpacing'>
|
|
46
|
+
} & Pick<FormProps<any>, 'outerSpacing'> &
|
|
47
|
+
Pick<QueryFormProps, 'types' | 'tags'>
|
|
56
48
|
>;
|
|
57
49
|
|
|
58
50
|
/**
|
|
@@ -68,6 +60,8 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
68
60
|
registry,
|
|
69
61
|
readonly,
|
|
70
62
|
showHeading = false,
|
|
63
|
+
types,
|
|
64
|
+
tags,
|
|
71
65
|
onQueryChanged,
|
|
72
66
|
onDelete,
|
|
73
67
|
outerSpacing = true,
|
|
@@ -84,18 +78,6 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
84
78
|
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
85
79
|
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
86
80
|
|
|
87
|
-
const serializedQuery = Match.value(mode).pipe(
|
|
88
|
-
Match.when('schema', () => getTypenameFromQuery(view.query.ast)),
|
|
89
|
-
Match.when('query', () => {
|
|
90
|
-
if (view.query.string) {
|
|
91
|
-
return view.query.string;
|
|
92
|
-
} else {
|
|
93
|
-
return 'Serializing query AST is not currently supported.';
|
|
94
|
-
}
|
|
95
|
-
}),
|
|
96
|
-
Match.exhaustive,
|
|
97
|
-
);
|
|
98
|
-
|
|
99
81
|
const queueTarget = Match.value(view.query.ast).pipe(
|
|
100
82
|
Match.when({ type: 'options' }, ({ options }) => {
|
|
101
83
|
return Option.fromNullable(options.queues).pipe(
|
|
@@ -111,12 +93,11 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
111
93
|
query:
|
|
112
94
|
mode === 'schema'
|
|
113
95
|
? Format.URL.annotations({ title: 'Record type' })
|
|
114
|
-
:
|
|
96
|
+
: QueryAST.Query.annotations({ title: 'Query' }),
|
|
115
97
|
});
|
|
116
98
|
|
|
117
|
-
if (mode === '
|
|
99
|
+
if (mode === 'tag') {
|
|
118
100
|
return Schema.Struct({
|
|
119
|
-
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
120
101
|
...base.fields,
|
|
121
102
|
target: Schema.optional(Schema.String.annotations({ title: 'Target Queue' })),
|
|
122
103
|
}).pipe(Schema.mutable);
|
|
@@ -124,11 +105,15 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
124
105
|
|
|
125
106
|
return base.pipe(Schema.mutable);
|
|
126
107
|
}, [mode]);
|
|
108
|
+
|
|
127
109
|
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
128
110
|
// TODO(burdon): Settings should have domain name owned by user.
|
|
129
111
|
const viewValues = useMemo(
|
|
130
|
-
() => ({
|
|
131
|
-
|
|
112
|
+
() => ({
|
|
113
|
+
query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
|
|
114
|
+
target: queueTarget,
|
|
115
|
+
}),
|
|
116
|
+
[mode, view.query.ast, queueTarget],
|
|
132
117
|
);
|
|
133
118
|
|
|
134
119
|
const handleToggleField = useCallback(
|
|
@@ -147,23 +132,13 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
147
132
|
}, [schema, projectionModel, readonly]);
|
|
148
133
|
|
|
149
134
|
const handleUpdate = useCallback(
|
|
150
|
-
(values:
|
|
151
|
-
invariant(!readonly);
|
|
135
|
+
(values: any) => {
|
|
152
136
|
requestAnimationFrame(() => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const queryChanged = serializedQuery !== values.query;
|
|
158
|
-
const targetValue = 'target' in values ? values.target : undefined;
|
|
159
|
-
const targetChanged = 'target' in values && values.target && queueTarget !== targetValue;
|
|
160
|
-
|
|
161
|
-
if ((queryChanged || targetChanged) && !readonly) {
|
|
162
|
-
onQueryChanged?.(values.query, targetValue);
|
|
163
|
-
}
|
|
137
|
+
const query = mode === 'schema' ? Query.select(Filter.typename(values.query)).ast : values.query;
|
|
138
|
+
onQueryChanged?.(query, values.target);
|
|
164
139
|
});
|
|
165
140
|
},
|
|
166
|
-
[
|
|
141
|
+
[onQueryChanged, view, queueTarget, mode],
|
|
167
142
|
);
|
|
168
143
|
|
|
169
144
|
const handleDelete = useCallback(
|
|
@@ -208,9 +183,12 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
208
183
|
[projectionModel],
|
|
209
184
|
);
|
|
210
185
|
|
|
186
|
+
const custom = useMemo(() => (mode === 'tag' ? customFields({ types, tags }) : undefined), [types, tags, mode]);
|
|
187
|
+
|
|
211
188
|
return (
|
|
212
189
|
<div role='none' className={mx(classNames)}>
|
|
213
|
-
{
|
|
190
|
+
{/* If readonlyProp is set, then the callout is not needed. */}
|
|
191
|
+
{schemaReadonly && !readonly && (
|
|
214
192
|
<Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
|
|
215
193
|
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
216
194
|
</Callout.Root>
|
|
@@ -222,10 +200,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
222
200
|
autoSave
|
|
223
201
|
schema={viewSchema}
|
|
224
202
|
values={viewValues}
|
|
225
|
-
readonly={readonly ? 'disabled-input' : false}
|
|
226
203
|
onSave={handleUpdate}
|
|
227
204
|
outerSpacing={outerSpacing}
|
|
228
|
-
Custom={
|
|
205
|
+
Custom={custom}
|
|
229
206
|
/>
|
|
230
207
|
|
|
231
208
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
@@ -255,7 +232,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
255
232
|
role='none'
|
|
256
233
|
className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}
|
|
257
234
|
>
|
|
258
|
-
<List.ItemDragHandle disabled={readonly} />
|
|
235
|
+
<List.ItemDragHandle disabled={readonly || schemaReadonly} />
|
|
259
236
|
<List.ItemTitle
|
|
260
237
|
classNames={hidden && 'text-subdued'}
|
|
261
238
|
onClick={() => handleToggleField(field)}
|
|
@@ -270,7 +247,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
270
247
|
disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
|
|
271
248
|
onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
|
|
272
249
|
/>
|
|
273
|
-
{
|
|
250
|
+
{!readonly && (
|
|
274
251
|
<>
|
|
275
252
|
<List.ItemDeleteButton
|
|
276
253
|
label={t('delete field label')}
|
|
@@ -291,7 +268,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
291
268
|
</>
|
|
292
269
|
)}
|
|
293
270
|
</div>
|
|
294
|
-
{expandedField === field.id &&
|
|
271
|
+
{expandedField === field.id && !readonly && (
|
|
295
272
|
<div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
|
|
296
273
|
<FieldEditor
|
|
297
274
|
readonly={readonly || schemaReadonly ? 'disabled-input' : false}
|
|
@@ -311,11 +288,11 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
311
288
|
</List.Root>
|
|
312
289
|
</div>
|
|
313
290
|
|
|
314
|
-
{!readonly && !expandedField &&
|
|
291
|
+
{!readonly && !expandedField && (
|
|
315
292
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
316
293
|
<IconButton
|
|
317
294
|
icon='ph--plus--regular'
|
|
318
|
-
label={t('
|
|
295
|
+
label={t('add property button label')}
|
|
319
296
|
onClick={readonly ? undefined : handleAdd}
|
|
320
297
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
321
298
|
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
@@ -328,51 +305,18 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
328
305
|
},
|
|
329
306
|
);
|
|
330
307
|
|
|
331
|
-
const customFields: Record<string, InputComponent>
|
|
308
|
+
const customFields = ({ types, tags }: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, InputComponent> => ({
|
|
332
309
|
query: (props: InputProps) => {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const elevation = useElevationContext();
|
|
337
|
-
|
|
338
|
-
const onValueChange = useRef(props.onValueChange);
|
|
339
|
-
useEffect(() => {
|
|
340
|
-
onValueChange.current = props.onValueChange;
|
|
341
|
-
}, [props.onValueChange]);
|
|
342
|
-
// NOTE: Including props.onValueChange in deps causes infinite loop with Editor.
|
|
343
|
-
const handleChange = useCallback((text: string) => onValueChange.current('string', text), []);
|
|
344
|
-
|
|
345
|
-
const extensions = useMemo(
|
|
346
|
-
() => [createBasicExtensions({ placeholder: t('query placeholder') }), createThemeExtensions({ themeMode })],
|
|
347
|
-
[],
|
|
310
|
+
const handleChange = useCallback(
|
|
311
|
+
(query: Query.Any) => props.onValueChange('object', query.ast),
|
|
312
|
+
[props.onValueChange],
|
|
348
313
|
);
|
|
349
314
|
|
|
350
|
-
// TODO(wittjosiah): This is probably not the right way to do these styles.
|
|
351
315
|
return (
|
|
352
316
|
<Input.Root>
|
|
353
317
|
<InputHeader label={props.label} />
|
|
354
|
-
<
|
|
355
|
-
classNames={mx(
|
|
356
|
-
inputTheme.input({ density, elevation }),
|
|
357
|
-
'flex items-center',
|
|
358
|
-
'focus-within:bg-focusSurface focus-within:border-separator focus-within:hover:bg-focusSurface',
|
|
359
|
-
)}
|
|
360
|
-
extensions={extensions}
|
|
361
|
-
value={props.getValue()}
|
|
362
|
-
onChange={handleChange}
|
|
363
|
-
/>
|
|
364
|
-
{/* TODO(wittjosiah): Support query editor.
|
|
365
|
-
<QueryEditor
|
|
366
|
-
classNames={mx(
|
|
367
|
-
inputTheme.input({ density, elevation }),
|
|
368
|
-
'flex items-center',
|
|
369
|
-
'focus-within:bg-focusSurface focus-within:border-separator focus-within:hover:bg-focusSurface',
|
|
370
|
-
)}
|
|
371
|
-
space={space}
|
|
372
|
-
value={props.getValue()}
|
|
373
|
-
onChange={handleChange}
|
|
374
|
-
/> */}
|
|
318
|
+
<QueryForm initialQuery={props.getValue()} types={types} tags={tags} onChange={handleChange} />
|
|
375
319
|
</Input.Root>
|
|
376
320
|
);
|
|
377
321
|
},
|
|
378
|
-
};
|
|
322
|
+
});
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type * as Schema from 'effect/Schema';
|
|
|
6
6
|
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
7
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { type
|
|
9
|
+
import { type AnyProperties, getValue, setValue } from '@dxos/echo/internal';
|
|
10
10
|
import { type JsonPath, type SimpleType, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { log } from '@dxos/log';
|
|
@@ -16,7 +16,7 @@ import { type MaybePromise } from '@dxos/util';
|
|
|
16
16
|
/**
|
|
17
17
|
* Return type from `useForm` hook.
|
|
18
18
|
*/
|
|
19
|
-
export type FormHandler<T extends
|
|
19
|
+
export type FormHandler<T extends AnyProperties> = {
|
|
20
20
|
//
|
|
21
21
|
// Form state management.
|
|
22
22
|
//
|
|
@@ -44,7 +44,7 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
44
44
|
/**
|
|
45
45
|
* Hook options.
|
|
46
46
|
*/
|
|
47
|
-
export interface FormOptions<T extends
|
|
47
|
+
export interface FormOptions<T extends AnyProperties> {
|
|
48
48
|
/**
|
|
49
49
|
* Effect schema (Type literal).
|
|
50
50
|
*/
|
|
@@ -85,7 +85,7 @@ export interface FormOptions<T extends BaseObject> {
|
|
|
85
85
|
* Creates a hook for managing form state, including values, validation, and submission.
|
|
86
86
|
* Deeply integrated with `@dxos/schema` for schema-based validation.
|
|
87
87
|
*/
|
|
88
|
-
export const useForm = <T extends
|
|
88
|
+
export const useForm = <T extends AnyProperties>({
|
|
89
89
|
schema,
|
|
90
90
|
initialValues,
|
|
91
91
|
onValuesChanged,
|
|
@@ -220,12 +220,13 @@ export const useForm = <T extends BaseObject>({
|
|
|
220
220
|
|
|
221
221
|
const newValues = { ...setValue(values, jsonPath, parsedValue) };
|
|
222
222
|
setValues(newValues);
|
|
223
|
-
|
|
223
|
+
const newChanged = { ...changed, [jsonPath]: true };
|
|
224
|
+
setChanged(newChanged);
|
|
224
225
|
onValuesChanged?.(newValues);
|
|
225
226
|
|
|
226
227
|
const isValid = validate(newValues);
|
|
227
228
|
if (isValid && onValid) {
|
|
228
|
-
onValid(newValues, { changed });
|
|
229
|
+
onValid(newValues, { changed: newChanged });
|
|
229
230
|
}
|
|
230
231
|
},
|
|
231
232
|
[values, onValuesChanged, validate, onValid, changed],
|
|
@@ -275,7 +276,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
275
276
|
);
|
|
276
277
|
};
|
|
277
278
|
|
|
278
|
-
const createKeySet = <T extends
|
|
279
|
+
const createKeySet = <T extends AnyProperties, V>(obj: T, value: V): Record<JsonPath, V> => {
|
|
279
280
|
invariant(obj);
|
|
280
281
|
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
|
|
281
282
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useEffect, useState } from 'react';
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type TypeAnnotation } from '@dxos/echo/internal';
|
|
8
8
|
import { type DXN } from '@dxos/keys';
|
|
@@ -25,9 +25,12 @@ type UseQueryRefOptionsProps = { refTypeInfo: TypeAnnotation | undefined; onQuer
|
|
|
25
25
|
* Hook to query reference options based on type information.
|
|
26
26
|
* Used internally within forms to fetch and format reference options for reference fields.
|
|
27
27
|
*/
|
|
28
|
+
// TODO(wittjosiah): This should be a reactive query so that the options are always up to date.
|
|
28
29
|
export const useQueryRefOptions = ({ refTypeInfo, onQueryRefOptions }: UseQueryRefOptionsProps) => {
|
|
29
30
|
const [options, setOptions] = useState<QueryTag[]>([]);
|
|
30
31
|
const [loading, setLoading] = useState(false);
|
|
32
|
+
const [state, setState] = useState({});
|
|
33
|
+
const update = useCallback(() => setState({}), []);
|
|
31
34
|
|
|
32
35
|
useEffect(() => {
|
|
33
36
|
if (!refTypeInfo || !onQueryRefOptions) {
|
|
@@ -53,7 +56,7 @@ export const useQueryRefOptions = ({ refTypeInfo, onQueryRefOptions }: UseQueryR
|
|
|
53
56
|
};
|
|
54
57
|
|
|
55
58
|
void fetchOptions();
|
|
56
|
-
}, [refTypeInfo, onQueryRefOptions]);
|
|
59
|
+
}, [refTypeInfo, onQueryRefOptions, state]);
|
|
57
60
|
|
|
58
|
-
return { options, loading };
|
|
61
|
+
return { options, update, loading };
|
|
59
62
|
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Obj } from '@dxos/echo';
|
|
8
|
+
import { type TypeAnnotation } from '@dxos/echo/internal';
|
|
8
9
|
import { Filter, type Space } from '@dxos/react-client/echo';
|
|
9
10
|
import { isNonNullable } from '@dxos/util';
|
|
10
11
|
|
|
@@ -18,18 +19,17 @@ export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): Que
|
|
|
18
19
|
if (!space) {
|
|
19
20
|
return [];
|
|
20
21
|
}
|
|
22
|
+
|
|
21
23
|
const query = space.db.query(Filter.typename(typeInfo.typename));
|
|
22
24
|
const { objects } = await query.run();
|
|
23
|
-
|
|
24
25
|
return objects
|
|
25
26
|
.map((object) => {
|
|
26
|
-
const dxn =
|
|
27
|
+
const dxn = Obj.getDXN(object);
|
|
27
28
|
if (!dxn) {
|
|
28
29
|
return undefined;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
const item = { dxn, label: object?.name ?? object?.id ?? '' };
|
|
32
|
+
const item = { dxn, label: Obj.getLabel(object) ?? object?.id ?? '' };
|
|
33
33
|
return item;
|
|
34
34
|
})
|
|
35
35
|
.filter(isNonNullable);
|
package/src/translations.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const translations = [
|
|
|
15
15
|
'empty readonly ref field label': '(none)',
|
|
16
16
|
|
|
17
17
|
// TODO(burdon): Standardize field/property.
|
|
18
|
-
'
|
|
18
|
+
'add property button label': 'Add property',
|
|
19
19
|
'field property label': 'Property',
|
|
20
20
|
'field property placeholder': 'Property name',
|
|
21
21
|
'field format label': 'Type',
|
|
@@ -33,8 +33,8 @@ export const translations = [
|
|
|
33
33
|
'ref field placeholder_other': 'Select items…',
|
|
34
34
|
|
|
35
35
|
// TODO(burdon): Factor out?
|
|
36
|
-
'button
|
|
37
|
-
'button
|
|
36
|
+
'cancel button label': 'Cancel',
|
|
37
|
+
'save button label': 'Save',
|
|
38
38
|
|
|
39
39
|
// FormatEnum
|
|
40
40
|
'format boolean': 'Boolean',
|
|
@@ -78,9 +78,9 @@ export const translations = [
|
|
|
78
78
|
'select option add': 'Add option',
|
|
79
79
|
|
|
80
80
|
// System schema message.
|
|
81
|
-
'system schema title': 'System
|
|
81
|
+
'system schema title': 'System type',
|
|
82
82
|
'system schema description':
|
|
83
|
-
'This is a system
|
|
83
|
+
'This is a system type and cannot be modified, though the view can still be configured.',
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
86
|
},
|
package/src/util/field.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Format, GeoLocation, TypeEnum } from '@dxos/echo/internal';
|
|
6
6
|
import { type ValidationError } from '@dxos/schema';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -13,7 +13,7 @@ import { type ValidationError } from '@dxos/schema';
|
|
|
13
13
|
*/
|
|
14
14
|
export type ParseProps = {
|
|
15
15
|
type?: TypeEnum;
|
|
16
|
-
format?:
|
|
16
|
+
format?: Format.TypeFormat;
|
|
17
17
|
value: any;
|
|
18
18
|
};
|
|
19
19
|
|
|
@@ -57,31 +57,31 @@ export const parseValue = ({ type, format, value }: ParseProps) => {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
switch (format) {
|
|
60
|
-
case
|
|
60
|
+
case Format.TypeFormat.Boolean:
|
|
61
61
|
return parseScalar(TypeEnum.Boolean);
|
|
62
62
|
|
|
63
|
-
case
|
|
64
|
-
case
|
|
65
|
-
case
|
|
63
|
+
case Format.TypeFormat.Number:
|
|
64
|
+
case Format.TypeFormat.Percent:
|
|
65
|
+
case Format.TypeFormat.Currency:
|
|
66
66
|
return parseScalar(TypeEnum.Number);
|
|
67
67
|
|
|
68
|
-
case
|
|
69
|
-
case
|
|
68
|
+
case Format.TypeFormat.String:
|
|
69
|
+
case Format.TypeFormat.Markdown: {
|
|
70
70
|
return parseScalar(TypeEnum.String);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
case
|
|
74
|
-
throw new Error(`unexpected format: ${
|
|
73
|
+
case Format.TypeFormat.Ref:
|
|
74
|
+
throw new Error(`unexpected format: ${Format.TypeFormat.Ref}`);
|
|
75
75
|
|
|
76
|
-
case
|
|
77
|
-
case
|
|
78
|
-
case
|
|
79
|
-
case
|
|
76
|
+
case Format.TypeFormat.DateTime:
|
|
77
|
+
case Format.TypeFormat.Date:
|
|
78
|
+
case Format.TypeFormat.Time:
|
|
79
|
+
case Format.TypeFormat.Timestamp: {
|
|
80
80
|
const date = new Date(value as string | number);
|
|
81
81
|
return isNaN(date.getTime()) ? null : date;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
case
|
|
84
|
+
case Format.TypeFormat.GeoPoint: {
|
|
85
85
|
// Parse string in format "lat,long" to GeoPoint [longitude, latitude].
|
|
86
86
|
if (typeof value === 'string') {
|
|
87
87
|
// Only keep digits, decimal points, minus signs, and commas.
|
|
@@ -108,23 +108,23 @@ export const parseValue = ({ type, format, value }: ParseProps) => {
|
|
|
108
108
|
// TODO(burdon): Type and format.
|
|
109
109
|
export type CellClassesForFieldTypeProps = {
|
|
110
110
|
type?: TypeEnum;
|
|
111
|
-
format?:
|
|
111
|
+
format?: Format.TypeFormat;
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
export const cellClassesForFieldType = ({ type, format }: CellClassesForFieldTypeProps): string[] | undefined => {
|
|
115
115
|
switch (format || type) {
|
|
116
|
-
case
|
|
116
|
+
case Format.TypeFormat.Markdown:
|
|
117
117
|
return undefined;
|
|
118
|
-
case
|
|
119
|
-
case
|
|
120
|
-
case
|
|
121
|
-
case
|
|
122
|
-
case
|
|
118
|
+
case Format.TypeFormat.Time:
|
|
119
|
+
case Format.TypeFormat.Timestamp:
|
|
120
|
+
case Format.TypeFormat.DateTime:
|
|
121
|
+
case Format.TypeFormat.Date:
|
|
122
|
+
case Format.TypeFormat.Duration:
|
|
123
123
|
return ['font-mono', 'text-right'];
|
|
124
|
-
case
|
|
124
|
+
case Format.TypeFormat.Currency:
|
|
125
125
|
return ['text-right'];
|
|
126
|
-
case
|
|
127
|
-
case
|
|
126
|
+
case Format.TypeFormat.JSON:
|
|
127
|
+
case Format.TypeFormat.DID:
|
|
128
128
|
return ['font-mono'];
|
|
129
129
|
case TypeEnum.Number:
|
|
130
130
|
case TypeEnum.Boolean:
|
package/src/util/formatting.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { format as formatDate } from 'date-fns/format';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Format, GeoLocation, TypeEnum } from '@dxos/echo/internal';
|
|
8
8
|
|
|
9
9
|
type ValueFormatProps = {
|
|
10
10
|
type: TypeEnum;
|
|
11
|
-
format?:
|
|
11
|
+
format?: Format.TypeFormat | undefined;
|
|
12
12
|
value: any;
|
|
13
13
|
locale?: string | undefined;
|
|
14
14
|
};
|
|
@@ -41,20 +41,20 @@ export const formatForDisplay = ({ type, format, value, locale = undefined }: Va
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
switch (format) {
|
|
44
|
-
case
|
|
44
|
+
case Format.TypeFormat.Boolean: {
|
|
45
45
|
return formatScalar(TypeEnum.Boolean);
|
|
46
46
|
}
|
|
47
|
-
case
|
|
47
|
+
case Format.TypeFormat.Number: {
|
|
48
48
|
return formatScalar(TypeEnum.Number);
|
|
49
49
|
}
|
|
50
|
-
case
|
|
51
|
-
case
|
|
50
|
+
case Format.TypeFormat.String:
|
|
51
|
+
case Format.TypeFormat.Ref: {
|
|
52
52
|
return formatScalar(TypeEnum.String);
|
|
53
53
|
}
|
|
54
|
-
case
|
|
54
|
+
case Format.TypeFormat.Percent: {
|
|
55
55
|
return `${(value as number) * 100}%`;
|
|
56
56
|
}
|
|
57
|
-
case
|
|
57
|
+
case Format.TypeFormat.Currency: {
|
|
58
58
|
// TODO(burdon): Get from property annotation.
|
|
59
59
|
return (value as number).toLocaleString(locale, {
|
|
60
60
|
style: 'currency',
|
|
@@ -63,28 +63,28 @@ export const formatForDisplay = ({ type, format, value, locale = undefined }: Va
|
|
|
63
63
|
maximumFractionDigits: 2,
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
|
-
case
|
|
66
|
+
case Format.TypeFormat.Date: {
|
|
67
67
|
try {
|
|
68
68
|
return formatDate(new Date(value as number), 'yyyy-MM-dd');
|
|
69
69
|
} catch (error) {
|
|
70
70
|
return 'Invalid Date';
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
case
|
|
73
|
+
case Format.TypeFormat.Time: {
|
|
74
74
|
try {
|
|
75
75
|
return formatDate(new Date(value as number), 'HH:mm:ss');
|
|
76
76
|
} catch (error) {
|
|
77
77
|
return 'Invalid Time';
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
case
|
|
80
|
+
case Format.TypeFormat.DateTime: {
|
|
81
81
|
try {
|
|
82
82
|
return formatDate(new Date(value as number), 'yyyy-MM-dd HH:mm:ss');
|
|
83
83
|
} catch (error) {
|
|
84
84
|
return 'Invalid DateTime';
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
case
|
|
87
|
+
case Format.TypeFormat.GeoPoint: {
|
|
88
88
|
if (value === null || value === undefined) {
|
|
89
89
|
return '';
|
|
90
90
|
}
|
|
@@ -125,31 +125,31 @@ export const formatForEditing = ({ type, format, value, locale = undefined }: Va
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
switch (format) {
|
|
128
|
-
case
|
|
129
|
-
case
|
|
130
|
-
case
|
|
131
|
-
case
|
|
128
|
+
case Format.TypeFormat.Boolean:
|
|
129
|
+
case Format.TypeFormat.Number:
|
|
130
|
+
case Format.TypeFormat.String:
|
|
131
|
+
case Format.TypeFormat.Ref: {
|
|
132
132
|
return formatScalar(type);
|
|
133
133
|
}
|
|
134
|
-
case
|
|
134
|
+
case Format.TypeFormat.Percent: {
|
|
135
135
|
return String(value * 100);
|
|
136
136
|
}
|
|
137
|
-
case
|
|
137
|
+
case Format.TypeFormat.Currency: {
|
|
138
138
|
return String(value);
|
|
139
139
|
}
|
|
140
|
-
case
|
|
140
|
+
case Format.TypeFormat.DateTime: {
|
|
141
141
|
const date = new Date(value as number);
|
|
142
142
|
return date.toISOString();
|
|
143
143
|
}
|
|
144
|
-
case
|
|
144
|
+
case Format.TypeFormat.Date: {
|
|
145
145
|
const date = new Date(value as number);
|
|
146
146
|
return date.toISOString().split('T')[0];
|
|
147
147
|
}
|
|
148
|
-
case
|
|
148
|
+
case Format.TypeFormat.Time: {
|
|
149
149
|
const date = new Date(value as number);
|
|
150
150
|
return date.toISOString().split('T')[1].split('.')[0];
|
|
151
151
|
}
|
|
152
|
-
case
|
|
152
|
+
case Format.TypeFormat.GeoPoint: {
|
|
153
153
|
// Handle null or undefined
|
|
154
154
|
if (value === null || value === undefined) {
|
|
155
155
|
return '';
|
package/src/util/refs.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type * as SchemaAST from 'effect/SchemaAST';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Format, ReferenceAnnotationId } from '@dxos/echo/internal';
|
|
8
8
|
import { findAnnotation } from '@dxos/effect';
|
|
9
9
|
import { type SchemaProperty } from '@dxos/schema';
|
|
10
10
|
|
|
@@ -18,11 +18,6 @@ type RefProps = {
|
|
|
18
18
|
export const getRefProps = (property: SchemaProperty<any>): RefProps | undefined => {
|
|
19
19
|
const { ast, format, array } = property;
|
|
20
20
|
|
|
21
|
-
// Direct reference.
|
|
22
|
-
if (format === FormatEnum.Ref) {
|
|
23
|
-
return { ast, isArray: false };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
21
|
// Array of references.
|
|
27
22
|
if (array) {
|
|
28
23
|
const elementType = findArrayElementType(ast);
|
|
@@ -34,5 +29,10 @@ export const getRefProps = (property: SchemaProperty<any>): RefProps | undefined
|
|
|
34
29
|
}
|
|
35
30
|
}
|
|
36
31
|
|
|
32
|
+
// Direct reference.
|
|
33
|
+
if (format === Format.TypeFormat.Ref) {
|
|
34
|
+
return { ast, isArray: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
37
|
return undefined;
|
|
38
38
|
};
|