@dxos/react-ui-form 0.8.4-main.8360d9e660 → 0.8.4-main.8baae0fced
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/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +1009 -612
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +82 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1009 -612
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +84 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +40 -12
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +76 -74
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +11 -5
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -6
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +5 -8
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +12 -6
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +28 -33
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -63
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -4
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +63 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +19 -0
- package/dist/types/src/util/field-type.d.ts.map +1 -0
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -1
- package/dist/types/src/util/properties.d.ts.map +1 -1
- package/dist/types/src/util/properties.test.d.ts +2 -0
- package/dist/types/src/util/properties.test.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +43 -41
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +4 -3
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +23 -36
- package/src/components/Form/Form.tsx +77 -48
- package/src/components/Form/FormField.tsx +27 -15
- package/src/components/Form/FormFieldComponent.tsx +6 -3
- package/src/components/Form/FormFieldSet.tsx +49 -32
- package/src/components/Form/fields/ArrayField.tsx +14 -8
- package/src/components/Form/fields/GeoPointField.tsx +8 -7
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +135 -45
- package/src/components/Form/fields/RefField.tsx +70 -35
- package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
- package/src/components/Form/fields/SelectOptionField.tsx +12 -12
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +114 -0
- package/src/components/ObjectForm/index.ts +5 -0
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +12 -10
- package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +148 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/AUDIT.md +51 -0
- package/src/components/Settings/Settings.stories.tsx +69 -21
- package/src/components/Settings/Settings.tsx +33 -183
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +58 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +12 -13
- package/src/components/ViewEditor/ViewEditor.test.tsx +0 -1
- package/src/components/ViewEditor/ViewEditor.tsx +25 -32
- package/src/components/index.ts +2 -0
- package/src/components/testing.tsx +19 -11
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/index.ts +0 -4
- package/src/translations.ts +66 -63
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -0
- package/src/util/properties.test.ts +63 -0
- package/src/util/properties.ts +14 -3
- package/src/util/refs.ts +7 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { composeStories } from '@storybook/react';
|
|
6
|
+
import { cleanup } from '@testing-library/react';
|
|
7
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { Filter, Obj, Tag } from '@dxos/echo';
|
|
10
|
+
|
|
11
|
+
import { OBJECT_PROPERTIES_DEBUG_SYMBOL } from '../testing';
|
|
12
|
+
import * as stories from './ObjectProperties.stories';
|
|
13
|
+
import { type ObjectPropertiesDebug } from './ObjectProperties.stories';
|
|
14
|
+
|
|
15
|
+
const { CreateTagPlay, CreateRefArrayPlay, CreateHiddenFieldPlay } = composeStories(stories);
|
|
16
|
+
|
|
17
|
+
const getDebug = (): ObjectPropertiesDebug => {
|
|
18
|
+
const debug = (window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] as ObjectPropertiesDebug | undefined;
|
|
19
|
+
expect(debug).toBeDefined();
|
|
20
|
+
return debug!;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('ObjectProperties — inline create flow', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
delete (window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('Tag.Tag: Create + Save persists tag and assigns it to the object', { timeout: 30_000 }, async () => {
|
|
30
|
+
await CreateTagPlay.run();
|
|
31
|
+
const { db, object } = getDebug();
|
|
32
|
+
|
|
33
|
+
// The new Tag should exist in the DB.
|
|
34
|
+
const tags = (await db.query(Filter.type(Tag.Tag)).run()) as Tag.Tag[];
|
|
35
|
+
const created = tags.find((tag) => tag.label === 'PinnedTag');
|
|
36
|
+
expect(created, 'new Tag.Tag with label "PinnedTag" should be in the database').toBeDefined();
|
|
37
|
+
|
|
38
|
+
// And `object.meta.tags` should contain a DXN that resolves to that tag.
|
|
39
|
+
// Compare by the DXN's object-id tail since refs may be stored as relative
|
|
40
|
+
// (`dxn:echo:@:<id>`) or absolute (`dxn:echo:<spaceId>:<id>`) forms.
|
|
41
|
+
const meta = Obj.getMeta(object);
|
|
42
|
+
const createdId = (created as any).id as string;
|
|
43
|
+
const matched = (meta.tags ?? []).some((dxn) => dxn.endsWith(`:${createdId}`));
|
|
44
|
+
expect(matched, "meta.tags should contain a DXN ending with the new tag's id").toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('non-Tag ref-array: Create + Save assigns the new ref to the array slot', { timeout: 30_000 }, async () => {
|
|
48
|
+
await CreateRefArrayPlay.run();
|
|
49
|
+
const { db, object } = getDebug();
|
|
50
|
+
|
|
51
|
+
// The new Author should exist in the DB.
|
|
52
|
+
const authors = (await db.query(Filter.typename('org.dxos.test.author' as any)).run()) as any[];
|
|
53
|
+
const created = authors.find((author: any) => author.name === 'Ada Lovelace');
|
|
54
|
+
expect(created, 'new Author with name "Ada Lovelace" should be in the database').toBeDefined();
|
|
55
|
+
|
|
56
|
+
// And the article's authors array should contain a Ref to it. This is the
|
|
57
|
+
// assertion that fails before the fix — handleCreate persists the object
|
|
58
|
+
// but never wires the new ref into the array slot.
|
|
59
|
+
const article = object as any;
|
|
60
|
+
expect(article.authors ?? [], 'article.authors should contain a single Ref').toHaveLength(1);
|
|
61
|
+
// Compare by DXN tail to be agnostic to relative vs absolute Ref form.
|
|
62
|
+
const createdId = (created as any).id as string;
|
|
63
|
+
expect(
|
|
64
|
+
article.authors[0]?.dxn?.toString().endsWith(`:${createdId}`),
|
|
65
|
+
'article.authors[0] should reference the newly-created Author',
|
|
66
|
+
).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test(
|
|
70
|
+
'hidden required field via FactoryAnnotation: Create + Save closes form and assigns ref',
|
|
71
|
+
{ timeout: 30_000 },
|
|
72
|
+
async () => {
|
|
73
|
+
await CreateHiddenFieldPlay.run();
|
|
74
|
+
const { db, object } = getDebug();
|
|
75
|
+
|
|
76
|
+
// The new Note should exist in the DB with the synthesised backing ref.
|
|
77
|
+
const notes = (await db.query(Filter.typename('org.dxos.test.note' as any)).run()) as any[];
|
|
78
|
+
const created = notes.find((note: any) => note.title === 'Ideas');
|
|
79
|
+
expect(created, 'new Note with title "Ideas" should be in the database').toBeDefined();
|
|
80
|
+
expect(
|
|
81
|
+
(created as any).backing,
|
|
82
|
+
'FactoryAnnotation should have populated the hidden `backing` ref',
|
|
83
|
+
).toBeDefined();
|
|
84
|
+
|
|
85
|
+
// The notebook's notes array should reference the new Note.
|
|
86
|
+
const notebook = object as any;
|
|
87
|
+
expect(notebook.notes ?? [], 'notebook.notes should contain a single Ref').toHaveLength(1);
|
|
88
|
+
const createdId = (created as any).id as string;
|
|
89
|
+
expect(
|
|
90
|
+
notebook.notes[0]?.dxn?.toString().endsWith(`:${createdId}`),
|
|
91
|
+
'notebook.notes[0] should reference the newly-created Note',
|
|
92
|
+
).toBe(true);
|
|
93
|
+
|
|
94
|
+
// The picker's create form should be gone.
|
|
95
|
+
expect(
|
|
96
|
+
document.querySelector('[data-testid="create-referenced-object-form"]'),
|
|
97
|
+
'create form should be dismissed after a successful save',
|
|
98
|
+
).toBeNull();
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Function from 'effect/Function';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
import React, { type PropsWithChildren, useCallback, useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
import { DXN, Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
11
|
+
import { useSchema } from '@dxos/echo-react';
|
|
12
|
+
import { type JsonPath, splitJsonPath } from '@dxos/echo/internal';
|
|
13
|
+
import { invariant } from '@dxos/invariant';
|
|
14
|
+
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
15
|
+
import { FactoryAnnotation } from '@dxos/schema';
|
|
16
|
+
import { composable, composableProps } from '@dxos/ui-theme';
|
|
17
|
+
import { isNonNullable } from '@dxos/util';
|
|
18
|
+
|
|
19
|
+
import { translationKey } from '#translations';
|
|
20
|
+
|
|
21
|
+
import { Form, type FormFieldMap, omitId } from '../Form';
|
|
22
|
+
|
|
23
|
+
export type ObjectPropertiesProps = PropsWithChildren<{ object: Obj.Unknown }>;
|
|
24
|
+
|
|
25
|
+
// TODO(wittjosiah): Reconcile w/ ObjectForm.
|
|
26
|
+
export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps>(
|
|
27
|
+
({ children, object, ...props }, forwardedRef) => {
|
|
28
|
+
const db = Obj.getDatabase(object);
|
|
29
|
+
const meta = Obj.getMeta(object);
|
|
30
|
+
const tags = (meta.tags ?? []).map((tag) => db?.makeRef(DXN.parse(tag))).filter(isNonNullable);
|
|
31
|
+
const values = useMemo(() => ({ tags, ...object }), [object, tags]);
|
|
32
|
+
|
|
33
|
+
// Obj.getSchema fails for database-registered (dynamic) schemas due to DXN mismatch;
|
|
34
|
+
// useSchema queries by typename which correctly resolves both static and dynamic schemas.
|
|
35
|
+
const typename = Obj.getTypename(object) ?? undefined;
|
|
36
|
+
const schemaFromRegistry = useSchema(db, typename);
|
|
37
|
+
const rawSchema = Obj.getSchema(object) ?? schemaFromRegistry;
|
|
38
|
+
|
|
39
|
+
const formSchema = useMemo(() => {
|
|
40
|
+
return Function.pipe(
|
|
41
|
+
rawSchema,
|
|
42
|
+
Option.fromNullable,
|
|
43
|
+
Option.map((schema) => omitId(BaseSchema.pipe(Schema.extend(schema)))),
|
|
44
|
+
Option.getOrUndefined,
|
|
45
|
+
);
|
|
46
|
+
}, [rawSchema]);
|
|
47
|
+
|
|
48
|
+
// Persist a newly-created object referenced by one of the form's ref
|
|
49
|
+
// fields and return it. The calling RefField wires the new Ref into its
|
|
50
|
+
// own slot's form value; for the BaseSchema `tags` array, the resulting
|
|
51
|
+
// form change is then synced back to `Obj.getMeta(object).tags` by
|
|
52
|
+
// `handleChange` below — so Tag.Tag follows the same generic path as any
|
|
53
|
+
// other ref-array field, no type-specific branching required here.
|
|
54
|
+
//
|
|
55
|
+
// Schemas whose required structure can't be produced by `Obj.make(schema,
|
|
56
|
+
// values)` alone (e.g. types with a required ref to a backing object) can
|
|
57
|
+
// declare a `FactoryAnnotation` to take over construction.
|
|
58
|
+
const handleCreate = useCallback(
|
|
59
|
+
(schema: Type.AnyEntity, values: any): Obj.Unknown => {
|
|
60
|
+
invariant(db);
|
|
61
|
+
invariant(Type.isObjectSchema(schema));
|
|
62
|
+
const factory = Option.getOrUndefined(FactoryAnnotation.get(schema));
|
|
63
|
+
const newObject = factory ? (factory(values) as Obj.Unknown) : Obj.make(schema, values);
|
|
64
|
+
return db.add(newObject) as Obj.Unknown;
|
|
65
|
+
},
|
|
66
|
+
[db],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// TODO(wittjosiah): Use FormRootProps type.
|
|
70
|
+
const handleChange = useCallback(
|
|
71
|
+
(
|
|
72
|
+
{ tags, ...values }: Schema.Schema.Type<typeof formSchema>,
|
|
73
|
+
{ isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> },
|
|
74
|
+
) => {
|
|
75
|
+
if (!isValid) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
|
|
80
|
+
|
|
81
|
+
// Handle tags separately using Obj.update.
|
|
82
|
+
const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
|
|
83
|
+
if (hasTagsChange) {
|
|
84
|
+
Obj.update(object, (object) => {
|
|
85
|
+
Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle other property changes.
|
|
90
|
+
const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
|
|
91
|
+
if (nonTagPaths.length > 0) {
|
|
92
|
+
Obj.update(object, () => {
|
|
93
|
+
for (const path of nonTagPaths) {
|
|
94
|
+
const parts = splitJsonPath(path);
|
|
95
|
+
const value = Obj.getValue(values as any, parts);
|
|
96
|
+
Obj.setValue(object, parts, value);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
[object],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!formSchema) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Form.Root
|
|
110
|
+
schema={formSchema}
|
|
111
|
+
defaultValues={values as any}
|
|
112
|
+
createTypename={Type.getTypename(Tag.Tag)}
|
|
113
|
+
createOptionIcon='ph--plus--regular'
|
|
114
|
+
createOptionLabel={['add-tag.label', { ns: translationKey }]}
|
|
115
|
+
createInitialValuePath='label'
|
|
116
|
+
createFieldMap={createFieldMap}
|
|
117
|
+
db={db}
|
|
118
|
+
onValuesChanged={handleChange}
|
|
119
|
+
onCreate={handleCreate}
|
|
120
|
+
>
|
|
121
|
+
<Form.Viewport {...composableProps(props)} ref={forwardedRef}>
|
|
122
|
+
<Form.Content>
|
|
123
|
+
<Form.FieldSet />
|
|
124
|
+
{children}
|
|
125
|
+
</Form.Content>
|
|
126
|
+
</Form.Viewport>
|
|
127
|
+
</Form.Root>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const createFieldMap: FormFieldMap = {
|
|
133
|
+
hue: ({ type, label, layout, getValue, onValueChange }) => {
|
|
134
|
+
const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
|
|
135
|
+
const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
|
|
136
|
+
return (
|
|
137
|
+
<>
|
|
138
|
+
{layout !== 'inline' && <Form.Label label={label} />}
|
|
139
|
+
<HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
|
|
140
|
+
</>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// TODO(wittjosiah): Would be nice to control order when extending so this isn't always first/last.
|
|
146
|
+
const BaseSchema = Schema.Struct({
|
|
147
|
+
tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional),
|
|
148
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Plugin Settings Audit
|
|
2
|
+
|
|
3
|
+
Tracks which plugin settings panels use schema-driven `Settings.FieldSet` and which still hand-roll their controls.
|
|
4
|
+
|
|
5
|
+
The target shape for a plugin settings panel is:
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<SettingsForm.Viewport>
|
|
9
|
+
<SettingsForm.Section title={t('settings.title', { ns: meta.id })}>
|
|
10
|
+
<SettingsForm.FieldSet
|
|
11
|
+
readonly={!onSettingsChange}
|
|
12
|
+
schema={Settings.Settings}
|
|
13
|
+
values={settings}
|
|
14
|
+
onValuesChanged={(values) => onSettingsChange?.(() => values)}
|
|
15
|
+
/>
|
|
16
|
+
</SettingsForm.Section>
|
|
17
|
+
</SettingsForm.Viewport>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Field labels and descriptions come from `.annotations({ title, description })` on each schema field. Use `fieldMap` for runtime-driven option lists (model pickers, etc.). Use the `visible` callback to gate fields on environment or sibling values.
|
|
21
|
+
|
|
22
|
+
## FieldSet adopters
|
|
23
|
+
|
|
24
|
+
| Plugin | Notes |
|
|
25
|
+
| ---------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `plugin-assistant` | Nested `modelDefaults` struct; `fieldMap` provides per-provider model selectors via `createSelectField`. |
|
|
27
|
+
| `plugin-crx` | Two booleans. |
|
|
28
|
+
| `plugin-deck` | Five booleans; `enableNativeRedirect` hidden via `visible` when `isSocket`. |
|
|
29
|
+
| `plugin-inbox` | Single boolean. |
|
|
30
|
+
| `plugin-markdown` | Original reference adopter. `snippets` text field gated via `visible`/`fieldMap`. |
|
|
31
|
+
| `plugin-meeting` | Single boolean. |
|
|
32
|
+
| `plugin-observability` | Boolean; info `Message.Root` banner kept above `FieldSet`. |
|
|
33
|
+
| `plugin-presenter` | Single boolean. |
|
|
34
|
+
| `plugin-sample` | Single boolean. |
|
|
35
|
+
| `plugin-script` | Mixed: auth `Button` retained as a manual `SettingsForm.Item`; `editorInputMode` rendered by `FieldSet`. |
|
|
36
|
+
| `plugin-sketch` | Boolean + `gridType` literal union (rendered as a Select — was previously a Switch). |
|
|
37
|
+
|
|
38
|
+
## Skipped
|
|
39
|
+
|
|
40
|
+
| Plugin | Reason |
|
|
41
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `plugin-debug` | Action buttons (download diagnostics, download logs, repair), `Toast`, and a storage-adapter selector wired to `client.config` rather than the plugin settings atom. |
|
|
43
|
+
| `plugin-space` | Renders a list of spaces with per-space "open settings" buttons — not bound to any schema field. |
|
|
44
|
+
| `plugin-spacetime` | Section is currently empty; nothing to render. |
|
|
45
|
+
| `plugin-thread` | Settings schema is `Schema.Struct({})` — nothing to render. |
|
|
46
|
+
|
|
47
|
+
## When to extend `SettingsFieldSet` vs. add a custom field
|
|
48
|
+
|
|
49
|
+
- **Extend `SettingsFieldSet`** when the rendering rule applies generally to a schema construct (e.g. nested structs recursing into sibling fields, literal unions becoming Selects). Keep auto-detection in `detectFieldType`.
|
|
50
|
+
- **Use `fieldMap`** when the schema can't fully describe the rendering — runtime-driven option lists, custom inputs, or override of the auto-rendered control. Use the typed form `SettingsFieldMap<T>` (or `satisfies SettingsFieldMap<Settings>`) so the entries are checked against the values shape.
|
|
51
|
+
- **Drop back to manual `SettingsForm.Item`** when the panel needs UI that isn't bound to a settings field at all (action buttons, info banners, lists of external entities). These coexist as siblings of `FieldSet` inside the same `Section`.
|
|
@@ -3,39 +3,40 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
6
7
|
import React from 'react';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { random } from '@dxos/random';
|
|
9
10
|
import { Button, Input } from '@dxos/react-ui';
|
|
10
11
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
12
|
|
|
12
|
-
import { translations } from '
|
|
13
|
+
import { translations } from '#translations';
|
|
13
14
|
|
|
14
15
|
import { Settings } from './Settings';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
random.seed(132);
|
|
17
18
|
|
|
18
19
|
const DefaultStory = () => {
|
|
19
20
|
return (
|
|
20
|
-
<Settings.
|
|
21
|
-
<Settings.Section title='Settings' description={
|
|
22
|
-
<Settings.
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
</Input.Root>
|
|
32
|
-
</Settings.ItemInput>
|
|
33
|
-
<Settings.ItemInput title={faker.lorem.sentence(3)} description={faker.lorem.paragraphs(2)}>
|
|
34
|
-
<Button>Test</Button>
|
|
35
|
-
</Settings.ItemInput>
|
|
36
|
-
</Settings.Group>
|
|
21
|
+
<Settings.Viewport>
|
|
22
|
+
<Settings.Section title='Settings' description={random.lorem.paragraphs(1)}>
|
|
23
|
+
<Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(1)}>
|
|
24
|
+
<Input.TextInput placeholder='Input' />
|
|
25
|
+
</Settings.Item>
|
|
26
|
+
<Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(2)}>
|
|
27
|
+
<Input.Switch />
|
|
28
|
+
</Settings.Item>
|
|
29
|
+
<Settings.Item title={random.lorem.sentence(3)} description={random.lorem.paragraphs(2)}>
|
|
30
|
+
<Button>Test</Button>
|
|
31
|
+
</Settings.Item>
|
|
37
32
|
</Settings.Section>
|
|
38
|
-
|
|
33
|
+
<Settings.Section title='Panel Example'>
|
|
34
|
+
<Settings.Panel>
|
|
35
|
+
<h3 className='text-lg mb-2'>Members</h3>
|
|
36
|
+
<p className='text-description'>Content inside a panel.</p>
|
|
37
|
+
</Settings.Panel>
|
|
38
|
+
</Settings.Section>
|
|
39
|
+
</Settings.Viewport>
|
|
39
40
|
);
|
|
40
41
|
};
|
|
41
42
|
|
|
@@ -54,3 +55,50 @@ export default meta;
|
|
|
54
55
|
type Story = StoryObj<typeof meta>;
|
|
55
56
|
|
|
56
57
|
export const Default: Story = {};
|
|
58
|
+
|
|
59
|
+
const TestViewModes = ['preview', 'readonly', 'source'] as const;
|
|
60
|
+
const TestViewMode = Schema.Union(...TestViewModes.map((mode) => Schema.Literal(mode)));
|
|
61
|
+
|
|
62
|
+
const TestSettings = Schema.mutable(
|
|
63
|
+
Schema.Struct({
|
|
64
|
+
viewMode: TestViewMode.annotations({ title: 'View mode', description: 'Default document view mode.' }),
|
|
65
|
+
toolbar: Schema.optional(
|
|
66
|
+
Schema.Boolean.annotations({ title: 'Show toolbar', description: 'Display formatting toolbar.' }),
|
|
67
|
+
),
|
|
68
|
+
fontSize: Schema.optional(
|
|
69
|
+
Schema.Number.annotations({ title: 'Font size', description: 'Editor font size in pixels.' }),
|
|
70
|
+
),
|
|
71
|
+
placeholder: Schema.optional(
|
|
72
|
+
Schema.String.annotations({ title: 'Placeholder', description: 'Default placeholder text.' }),
|
|
73
|
+
),
|
|
74
|
+
debug: Schema.optional(Schema.Boolean.annotations({ title: 'Debug mode', description: 'Enable debug features.' })),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
type TestSettings = Schema.Schema.Type<typeof TestSettings>;
|
|
79
|
+
|
|
80
|
+
const FieldSetStory = () => {
|
|
81
|
+
const [values, setValues] = React.useState<TestSettings>({
|
|
82
|
+
viewMode: 'preview',
|
|
83
|
+
toolbar: true,
|
|
84
|
+
fontSize: 14,
|
|
85
|
+
debug: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Settings.Viewport>
|
|
90
|
+
<Settings.Section title='Plugin Settings (Auto-generated)'>
|
|
91
|
+
<Settings.FieldSet
|
|
92
|
+
schema={TestSettings}
|
|
93
|
+
values={values}
|
|
94
|
+
onValuesChanged={setValues}
|
|
95
|
+
visible={(path, values) => path !== 'placeholder' || !!values.debug}
|
|
96
|
+
/>
|
|
97
|
+
</Settings.Section>
|
|
98
|
+
</Settings.Viewport>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const FieldSet: Story = {
|
|
103
|
+
render: FieldSetStory,
|
|
104
|
+
};
|