@dxos/react-ui-form 0.8.4-main.422d1c7879 → 0.8.4-main.4f23b4e393
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 +148 -273
- 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 +148 -273
- 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 -63
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +19 -3
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +72 -72
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +4 -4
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +5 -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 +4 -7
- 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.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +64 -63
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
- 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 -63
- 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.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.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 +63 -63
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +63 -63
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.d.ts +10 -2
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +63 -63
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +15 -10
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Settings/SettingsItem.d.ts +8 -9
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -1
- 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 +1 -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 +63 -63
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- 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.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 -63
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +1 -1
- package/dist/types/src/util/field-type.d.ts.map +1 -1
- 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/properties.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -30
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -1
- package/src/components/FieldEditor/FieldEditor.tsx +3 -2
- package/src/components/Form/Form.stories.tsx +2 -1
- package/src/components/Form/Form.tsx +3 -3
- package/src/components/Form/FormField.tsx +2 -1
- package/src/components/Form/FormFieldComponent.tsx +3 -3
- package/src/components/Form/fields/ArrayField.tsx +6 -9
- package/src/components/Form/fields/GeoPointField.tsx +3 -2
- package/src/components/Form/fields/MarkdownField.stories.tsx +56 -1
- package/src/components/Form/fields/MarkdownField.tsx +2 -1
- package/src/components/Form/fields/RefField.tsx +4 -3
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -1
- package/src/components/Form/fields/SelectOptionField.tsx +6 -5
- package/src/components/ObjectForm/ObjectForm.tsx +6 -5
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +8 -5
- package/src/components/ObjectPicker/ObjectPicker.tsx +18 -9
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +2 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +5 -4
- package/src/components/Settings/AUDIT.md +52 -0
- package/src/components/Settings/Settings.stories.tsx +2 -1
- package/src/components/Settings/Settings.tsx +3 -4
- package/src/components/Settings/SettingsFieldSet.tsx +55 -9
- package/src/components/Settings/SettingsItem.tsx +4 -8
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +1 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +7 -7
- package/src/components/ViewEditor/ViewEditor.tsx +17 -24
- package/src/components/testing.tsx +1 -1
- package/src/index.ts +0 -4
- package/src/util/field-type.ts +4 -1
|
@@ -15,7 +15,8 @@ import { FactoryAnnotation } from '@dxos/schema';
|
|
|
15
15
|
import { composable, composableProps } from '@dxos/ui-theme';
|
|
16
16
|
import { isNonNullable } from '@dxos/util';
|
|
17
17
|
|
|
18
|
-
import { translationKey } from '
|
|
18
|
+
import { translationKey } from '#translations';
|
|
19
|
+
|
|
19
20
|
import { Form, type FormFieldMap, omitId } from '../Form';
|
|
20
21
|
|
|
21
22
|
export type ObjectPropertiesProps = PropsWithChildren<{ object: Obj.Unknown }>;
|
|
@@ -70,10 +71,10 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
|
|
|
70
71
|
|
|
71
72
|
const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
|
|
72
73
|
|
|
73
|
-
// Handle tags separately using Obj.
|
|
74
|
+
// Handle tags separately using Obj.update.
|
|
74
75
|
const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
|
|
75
76
|
if (hasTagsChange) {
|
|
76
|
-
Obj.
|
|
77
|
+
Obj.update(object, (object) => {
|
|
77
78
|
Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
|
|
78
79
|
});
|
|
79
80
|
}
|
|
@@ -81,7 +82,7 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
|
|
|
81
82
|
// Handle other property changes.
|
|
82
83
|
const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
|
|
83
84
|
if (nonTagPaths.length > 0) {
|
|
84
|
-
Obj.
|
|
85
|
+
Obj.update(object, () => {
|
|
85
86
|
for (const path of nonTagPaths) {
|
|
86
87
|
const parts = splitJsonPath(path);
|
|
87
88
|
const value = Obj.getValue(values as any, parts);
|
|
@@ -0,0 +1,52 @@
|
|
|
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-files` | `IconButton` actions (select root, export, import) and gating on non-schema state (`state.rootHandle`). |
|
|
44
|
+
| `plugin-space` | Renders a list of spaces with per-space "open settings" buttons — not bound to any schema field. |
|
|
45
|
+
| `plugin-spacetime` | Section is currently empty; nothing to render. |
|
|
46
|
+
| `plugin-thread` | Settings schema is `Schema.Struct({})` — nothing to render. |
|
|
47
|
+
|
|
48
|
+
## When to extend `SettingsFieldSet` vs. add a custom field
|
|
49
|
+
|
|
50
|
+
- **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`.
|
|
51
|
+
- **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.
|
|
52
|
+
- **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`.
|
|
@@ -10,7 +10,8 @@ import { random } from '@dxos/random';
|
|
|
10
10
|
import { Button, Input } from '@dxos/react-ui';
|
|
11
11
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
12
|
|
|
13
|
-
import { translations } from '
|
|
13
|
+
import { translations } from '#translations';
|
|
14
|
+
|
|
14
15
|
import { Settings } from './Settings';
|
|
15
16
|
|
|
16
17
|
random.seed(132);
|
|
@@ -7,7 +7,8 @@ import React, { type PropsWithChildren } from 'react';
|
|
|
7
7
|
import { type Label, ScrollArea, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
8
|
import { composable, composableProps, mx } from '@dxos/ui-theme';
|
|
9
9
|
|
|
10
|
-
import { translationKey } from '
|
|
10
|
+
import { translationKey } from '#translations';
|
|
11
|
+
|
|
11
12
|
import { SettingsFieldSet } from './SettingsFieldSet';
|
|
12
13
|
import { SettingsItem, SettingsPanel } from './SettingsItem';
|
|
13
14
|
|
|
@@ -35,9 +36,7 @@ const SettingsViewport = composable<HTMLDivElement>(({ children, ...props }, for
|
|
|
35
36
|
return (
|
|
36
37
|
<ScrollArea.Root {...composableProps(props)} orientation='vertical' centered thin ref={forwardedRef}>
|
|
37
38
|
<ScrollArea.Viewport classNames='px-4 pointer-coarse:px-2'>
|
|
38
|
-
<div
|
|
39
|
-
{children}
|
|
40
|
-
</div>
|
|
39
|
+
<div className='dx-document flex flex-col gap-2 py-4'>{children}</div>
|
|
41
40
|
</ScrollArea.Viewport>
|
|
42
41
|
</ScrollArea.Root>
|
|
43
42
|
);
|
|
@@ -27,9 +27,14 @@ export type SettingsFieldProps<T = any> = {
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Map of property names to custom field
|
|
30
|
+
* Map of property names to either a custom field renderer or a nested field map for struct fields.
|
|
31
|
+
* When a value type `T` is provided, the map shape is checked against the schema's value type.
|
|
31
32
|
*/
|
|
32
|
-
export type SettingsFieldMap = Record<string,
|
|
33
|
+
export type SettingsFieldMap<T extends Record<string, any> = Record<string, any>> = {
|
|
34
|
+
[K in keyof T]?: NonNullable<T[K]> extends Record<string, any>
|
|
35
|
+
? SettingsFieldMap<NonNullable<T[K]>>
|
|
36
|
+
: React.FC<SettingsFieldProps<T[K]>>;
|
|
37
|
+
};
|
|
33
38
|
|
|
34
39
|
export type SettingsFieldSetProps<T extends Record<string, any> = Record<string, any>> = {
|
|
35
40
|
/** Effect Schema for the settings object. */
|
|
@@ -44,14 +49,17 @@ export type SettingsFieldSetProps<T extends Record<string, any> = Record<string,
|
|
|
44
49
|
/** When true, all controls are disabled. */
|
|
45
50
|
readonly?: boolean;
|
|
46
51
|
|
|
47
|
-
/** Map of property names to custom field renderers. */
|
|
48
|
-
fieldMap?: SettingsFieldMap
|
|
52
|
+
/** Map of property names to custom field renderers. Nested objects map to struct sub-fields. */
|
|
53
|
+
fieldMap?: SettingsFieldMap<T>;
|
|
49
54
|
|
|
50
|
-
/** Control field visibility.
|
|
55
|
+
/** Control field visibility. Receives the dotted path (e.g. `'modelDefaults.edge'`) at every level. */
|
|
51
56
|
visible?: (path: string, values: T) => boolean;
|
|
52
57
|
|
|
53
|
-
/** Override the order of fields. Fields not listed are appended in schema order. */
|
|
58
|
+
/** Override the order of fields. Fields not listed are appended in schema order. Applied per scope. */
|
|
54
59
|
sort?: string[];
|
|
60
|
+
|
|
61
|
+
/** Internal: dot-path prefix used by recursive struct rendering. */
|
|
62
|
+
pathPrefix?: string;
|
|
55
63
|
};
|
|
56
64
|
|
|
57
65
|
//
|
|
@@ -66,6 +74,7 @@ export const SettingsFieldSet = <T extends Record<string, any>>({
|
|
|
66
74
|
fieldMap,
|
|
67
75
|
visible,
|
|
68
76
|
sort,
|
|
77
|
+
pathPrefix = '',
|
|
69
78
|
}: SettingsFieldSetProps<T>) => {
|
|
70
79
|
const properties = useMemo(() => {
|
|
71
80
|
const props = getFormProperties(schema.ast);
|
|
@@ -90,18 +99,27 @@ export const SettingsFieldSet = <T extends Record<string, any>>({
|
|
|
90
99
|
<>
|
|
91
100
|
{properties.map((property) => {
|
|
92
101
|
const name = property.name.toString();
|
|
93
|
-
|
|
102
|
+
const path = pathPrefix ? `${pathPrefix}.${name}` : name;
|
|
103
|
+
if (visible && !visible(path, values)) {
|
|
94
104
|
return null;
|
|
95
105
|
}
|
|
96
106
|
|
|
107
|
+
const entry = fieldMap?.[name as keyof typeof fieldMap] as
|
|
108
|
+
| React.FC<SettingsFieldProps>
|
|
109
|
+
| SettingsFieldMap
|
|
110
|
+
| undefined;
|
|
97
111
|
return (
|
|
98
112
|
<SettingsFieldItem
|
|
99
113
|
key={name}
|
|
114
|
+
path={path}
|
|
100
115
|
property={property}
|
|
101
116
|
value={values[name]}
|
|
102
117
|
onChange={(value) => handleChange(name, value)}
|
|
103
118
|
readonly={readonly}
|
|
104
|
-
customField={
|
|
119
|
+
customField={typeof entry === 'function' ? entry : undefined}
|
|
120
|
+
nestedFieldMap={typeof entry === 'object' ? entry : undefined}
|
|
121
|
+
visible={visible as (path: string, values: any) => boolean}
|
|
122
|
+
sort={sort}
|
|
105
123
|
/>
|
|
106
124
|
);
|
|
107
125
|
})}
|
|
@@ -117,13 +135,27 @@ SettingsFieldSet.displayName = 'Settings.FieldSet';
|
|
|
117
135
|
|
|
118
136
|
type SettingsFieldItemProps = {
|
|
119
137
|
property: SchemaProperty;
|
|
138
|
+
path: string;
|
|
120
139
|
value: any;
|
|
121
140
|
onChange: (value: any) => void;
|
|
122
141
|
readonly?: boolean;
|
|
123
142
|
customField?: React.FC<SettingsFieldProps>;
|
|
143
|
+
nestedFieldMap?: SettingsFieldMap;
|
|
144
|
+
visible?: (path: string, values: any) => boolean;
|
|
145
|
+
sort?: string[];
|
|
124
146
|
};
|
|
125
147
|
|
|
126
|
-
const SettingsFieldItem = ({
|
|
148
|
+
const SettingsFieldItem = ({
|
|
149
|
+
property,
|
|
150
|
+
path,
|
|
151
|
+
value,
|
|
152
|
+
onChange,
|
|
153
|
+
readonly,
|
|
154
|
+
customField,
|
|
155
|
+
nestedFieldMap,
|
|
156
|
+
visible,
|
|
157
|
+
sort,
|
|
158
|
+
}: SettingsFieldItemProps) => {
|
|
127
159
|
const { type } = property;
|
|
128
160
|
const name = property.name.toString();
|
|
129
161
|
const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type) ?? String.capitalize(name);
|
|
@@ -142,6 +174,20 @@ const SettingsFieldItem = ({ property, value, onChange, readonly, customField }:
|
|
|
142
174
|
const fieldType = detectFieldType(type);
|
|
143
175
|
|
|
144
176
|
switch (fieldType) {
|
|
177
|
+
case 'struct':
|
|
178
|
+
return (
|
|
179
|
+
<SettingsFieldSet
|
|
180
|
+
schema={{ ast: type }}
|
|
181
|
+
values={value ?? {}}
|
|
182
|
+
onValuesChanged={onChange}
|
|
183
|
+
readonly={readonly}
|
|
184
|
+
fieldMap={nestedFieldMap}
|
|
185
|
+
visible={visible}
|
|
186
|
+
sort={sort}
|
|
187
|
+
pathPrefix={path}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
|
|
145
191
|
case 'boolean':
|
|
146
192
|
return (
|
|
147
193
|
<SettingsItem title={title} description={description}>
|
|
@@ -7,7 +7,7 @@ import React, { type PropsWithChildren } from 'react';
|
|
|
7
7
|
import { Input, type Label, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
8
|
import { mx } from '@dxos/ui-theme';
|
|
9
9
|
|
|
10
|
-
import { translationKey } from '
|
|
10
|
+
import { translationKey } from '#translations';
|
|
11
11
|
|
|
12
12
|
const SETTINGS_PANEL_NAME = 'Settings.Panel';
|
|
13
13
|
const SETTINGS_ITEM_NAME = 'Settings.Item';
|
|
@@ -20,9 +20,7 @@ type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
|
|
|
20
20
|
|
|
21
21
|
export const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
|
|
22
22
|
return (
|
|
23
|
-
<div
|
|
24
|
-
{children}
|
|
25
|
-
</div>
|
|
23
|
+
<div className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>{children}</div>
|
|
26
24
|
);
|
|
27
25
|
};
|
|
28
26
|
|
|
@@ -43,7 +41,7 @@ export const SettingsItem = ({ title, description = '', children }: SettingsItem
|
|
|
43
41
|
return (
|
|
44
42
|
<Input.Root>
|
|
45
43
|
<SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
|
|
46
|
-
<Input.Label classNames='pb-trim-md text-base-
|
|
44
|
+
<Input.Label classNames='pb-trim-md text-base-foreground text-lg md:col-span-2'>
|
|
47
45
|
{toLocalizedString(title, t)}
|
|
48
46
|
</Input.Label>
|
|
49
47
|
<Input.DescriptionAndValidation>
|
|
@@ -51,9 +49,7 @@ export const SettingsItem = ({ title, description = '', children }: SettingsItem
|
|
|
51
49
|
{toLocalizedString(description, t)}
|
|
52
50
|
</Input.Description>
|
|
53
51
|
</Input.DescriptionAndValidation>
|
|
54
|
-
<div
|
|
55
|
-
{children}
|
|
56
|
-
</div>
|
|
52
|
+
<div className='text-end py-1'>{children}</div>
|
|
57
53
|
</SettingsPanel>
|
|
58
54
|
</Input.Root>
|
|
59
55
|
);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { Select } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type SettingsFieldProps } from './SettingsFieldSet';
|
|
10
|
+
|
|
11
|
+
export type SelectFieldOption = {
|
|
12
|
+
value: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CreateSelectFieldOptions = {
|
|
17
|
+
/** Options to display. Strings are used as both value and label. */
|
|
18
|
+
options: ReadonlyArray<string | SelectFieldOption>;
|
|
19
|
+
/** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
|
|
20
|
+
defaultLabel?: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Factory for a `SettingsFieldSet` `fieldMap` entry that renders a Select control
|
|
25
|
+
* over a fixed list of options, with an optional sentinel for `undefined`.
|
|
26
|
+
*/
|
|
27
|
+
export const createSelectField = ({
|
|
28
|
+
options,
|
|
29
|
+
defaultLabel = 'Default',
|
|
30
|
+
}: CreateSelectFieldOptions): React.FC<SettingsFieldProps<string | undefined>> => {
|
|
31
|
+
const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
|
|
32
|
+
const hasDefault = defaultLabel !== null;
|
|
33
|
+
// Per-instance sentinel avoids collisions with real option values; empty string when the
|
|
34
|
+
// sentinel is omitted so `undefined` maps to a value Select.Root treats as "no selection".
|
|
35
|
+
const sentinel = hasDefault ? `__default__${Math.random().toString(36).slice(2)}` : '';
|
|
36
|
+
|
|
37
|
+
return ({ value, onChange, readonly }) => (
|
|
38
|
+
<Select.Root
|
|
39
|
+
disabled={readonly}
|
|
40
|
+
value={value ?? sentinel}
|
|
41
|
+
onValueChange={(next) => onChange(hasDefault && next === sentinel ? undefined : next)}
|
|
42
|
+
>
|
|
43
|
+
<Select.TriggerButton disabled={readonly} />
|
|
44
|
+
<Select.Portal>
|
|
45
|
+
<Select.Content>
|
|
46
|
+
<Select.Viewport>
|
|
47
|
+
{hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
|
|
48
|
+
{normalized.map((option) => (
|
|
49
|
+
<Select.Option key={option.value} value={option.value}>
|
|
50
|
+
{option.label ?? option.value}
|
|
51
|
+
</Select.Option>
|
|
52
|
+
))}
|
|
53
|
+
</Select.Viewport>
|
|
54
|
+
<Select.Arrow />
|
|
55
|
+
</Select.Content>
|
|
56
|
+
</Select.Portal>
|
|
57
|
+
</Select.Root>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -6,8 +6,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
|
|
10
|
-
import { type View } from '@dxos/echo';
|
|
9
|
+
import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type, type View } from '@dxos/echo';
|
|
11
10
|
import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
|
|
12
11
|
import { useQuery } from '@dxos/react-client/echo';
|
|
13
12
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
@@ -16,7 +15,8 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
|
16
15
|
import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
|
|
17
16
|
import { Employer, Organization, Person, Pipeline } from '@dxos/types';
|
|
18
17
|
|
|
19
|
-
import { translations } from '
|
|
18
|
+
import { translations } from '#translations';
|
|
19
|
+
|
|
20
20
|
import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
21
21
|
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
22
22
|
|
|
@@ -89,8 +89,8 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
89
89
|
|
|
90
90
|
if (props.mode === 'tag') {
|
|
91
91
|
const queue = target && DXN.tryParse(target) ? target : undefined;
|
|
92
|
-
const query = queue ? Query.fromAst(newQuery).from({
|
|
93
|
-
Obj.
|
|
92
|
+
const query = queue ? Query.fromAst(newQuery).from({ feeds: [queue] }) : Query.fromAst(newQuery);
|
|
93
|
+
Obj.update(view, (view) => {
|
|
94
94
|
view.query.ast = query.ast as Mutable<typeof query.ast>;
|
|
95
95
|
});
|
|
96
96
|
|
|
@@ -104,12 +104,12 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
104
104
|
query,
|
|
105
105
|
jsonSchema: newSchema.jsonSchema,
|
|
106
106
|
});
|
|
107
|
-
Obj.
|
|
107
|
+
Obj.update(view, (view) => {
|
|
108
108
|
view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
|
|
109
109
|
});
|
|
110
110
|
setSchema(() => newSchema);
|
|
111
111
|
} else {
|
|
112
|
-
Obj.
|
|
112
|
+
Obj.update(view, (view) => {
|
|
113
113
|
view.query.ast = newQuery as Mutable<typeof newQuery>;
|
|
114
114
|
});
|
|
115
115
|
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
@@ -9,8 +9,7 @@ import * as Option from 'effect/Option';
|
|
|
9
9
|
import * as Schema from 'effect/Schema';
|
|
10
10
|
import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
|
|
11
11
|
|
|
12
|
-
import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry } from '@dxos/echo';
|
|
13
|
-
import { View } from '@dxos/echo';
|
|
12
|
+
import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry, View } from '@dxos/echo';
|
|
14
13
|
import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
15
14
|
import { invariant } from '@dxos/invariant';
|
|
16
15
|
import { useObject, useQuery } from '@dxos/react-client/echo';
|
|
@@ -24,9 +23,10 @@ import {
|
|
|
24
23
|
createEchoChangeCallback,
|
|
25
24
|
getTypenameFromQuery,
|
|
26
25
|
} from '@dxos/schema';
|
|
27
|
-
import { mx, osTranslations
|
|
26
|
+
import { mx, osTranslations } from '@dxos/ui-theme';
|
|
27
|
+
|
|
28
|
+
import { translationKey } from '#translations';
|
|
28
29
|
|
|
29
|
-
import { translationKey } from '../../translations';
|
|
30
30
|
import { FieldEditor } from '../FieldEditor';
|
|
31
31
|
import {
|
|
32
32
|
Form,
|
|
@@ -100,8 +100,8 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
100
100
|
if (from._tag !== 'scope') {
|
|
101
101
|
return undefined;
|
|
102
102
|
}
|
|
103
|
-
return Option.fromNullable(from.scope.
|
|
104
|
-
Option.flatMap((
|
|
103
|
+
return Option.fromNullable(from.scope.feeds).pipe(
|
|
104
|
+
Option.flatMap((feeds) => Array.head(feeds)),
|
|
105
105
|
Option.map(String),
|
|
106
106
|
Option.getOrUndefined,
|
|
107
107
|
);
|
|
@@ -160,13 +160,13 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
160
160
|
const handleUpdate = useCallback(
|
|
161
161
|
(values: any) => {
|
|
162
162
|
const targetValue = values.target;
|
|
163
|
-
let
|
|
163
|
+
let feedDXN: string | undefined;
|
|
164
164
|
|
|
165
165
|
if (Ref.isRef(targetValue)) {
|
|
166
|
-
const
|
|
167
|
-
const feed = feeds.find((feed) => Obj.getDXN(feed).toString() ===
|
|
166
|
+
const targetDXN = targetValue.dxn.toString();
|
|
167
|
+
const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === targetDXN);
|
|
168
168
|
if (feed) {
|
|
169
|
-
|
|
169
|
+
feedDXN = Feed.getQueueDxn(feed)?.toString();
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -175,7 +175,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
175
175
|
mode === 'schema'
|
|
176
176
|
? Query.select(Filter.typename(values.query)).ast
|
|
177
177
|
: JSON.parse(JSON.stringify(values.query));
|
|
178
|
-
onQueryChanged?.(query,
|
|
178
|
+
onQueryChanged?.(query, feedDXN);
|
|
179
179
|
},
|
|
180
180
|
[onQueryChanged, mode, feeds],
|
|
181
181
|
);
|
|
@@ -189,7 +189,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
189
189
|
);
|
|
190
190
|
|
|
191
191
|
return (
|
|
192
|
-
<div
|
|
192
|
+
<div className={mx(classNames)}>
|
|
193
193
|
{/* If readonly is set, then the callout is not needed. */}
|
|
194
194
|
{schemaReadonly && !readonly && (
|
|
195
195
|
<Message.Root valence='info' classNames='my-form-padding'>
|
|
@@ -274,7 +274,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
274
274
|
const handleMove = useCallback(
|
|
275
275
|
(fromIndex: number, toIndex: number) => {
|
|
276
276
|
invariant(!readonly);
|
|
277
|
-
Obj.
|
|
277
|
+
Obj.update(view, (view) => {
|
|
278
278
|
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
279
279
|
const fields = [...view.projection.fields];
|
|
280
280
|
const [moved] = fields.splice(fromIndex, 1);
|
|
@@ -328,20 +328,13 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
328
328
|
classNames={'grid grid-cols-subgrid col-span-5'}
|
|
329
329
|
aria-expanded={expandedField === field.id}
|
|
330
330
|
>
|
|
331
|
-
<div
|
|
332
|
-
role='none'
|
|
333
|
-
className={mx(
|
|
334
|
-
subtleHover,
|
|
335
|
-
'grid grid-cols-subgrid col-span-5',
|
|
336
|
-
'rounded-xs cursor-pointer min-h-10',
|
|
337
|
-
)}
|
|
338
|
-
>
|
|
331
|
+
<div className='grid grid-cols-subgrid col-span-5 rounded-xs cursor-pointer min-h-10 dx-hover'>
|
|
339
332
|
<List.ItemDragHandle disabled={readonly || schemaReadonly} />
|
|
340
333
|
<List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
|
|
341
334
|
{field.path}
|
|
342
335
|
</List.ItemTitle>
|
|
343
336
|
<List.ItemIconButton
|
|
344
|
-
label={t(hidden ? 'show
|
|
337
|
+
label={t(hidden ? 'show-field.label' : 'hide-field.label')}
|
|
345
338
|
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
346
339
|
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
347
340
|
autoHide={false}
|
|
@@ -369,7 +362,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
369
362
|
)}
|
|
370
363
|
</div>
|
|
371
364
|
{expandedField === field.id && !readonly && (
|
|
372
|
-
<div
|
|
365
|
+
<div className='col-span-full mt-1 mb-1 border border-separator rounded-md'>
|
|
373
366
|
<FieldEditor
|
|
374
367
|
readonly={readonly || schemaReadonly}
|
|
375
368
|
registry={registry}
|
|
@@ -384,7 +377,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
384
377
|
})}
|
|
385
378
|
</div>
|
|
386
379
|
{!readonly && !expandedField && (
|
|
387
|
-
<div
|
|
380
|
+
<div className='my-form-padding'>
|
|
388
381
|
<IconButton
|
|
389
382
|
icon='ph--plus--regular'
|
|
390
383
|
label={t('add-property-button.label')}
|
|
@@ -12,7 +12,7 @@ type TestLayoutProps = PropsWithChildren<{ json?: any }>;
|
|
|
12
12
|
|
|
13
13
|
export const TestLayout = ({ children, json }: TestLayoutProps) => {
|
|
14
14
|
return (
|
|
15
|
-
<div
|
|
15
|
+
<div className='dx-container grid grid-cols-[1fr_1fr] p-4 gap-4'>
|
|
16
16
|
<TestPanel>{children}</TestPanel>
|
|
17
17
|
<TestPanel>
|
|
18
18
|
<Syntax.Root data={json}>
|
package/src/index.ts
CHANGED
package/src/util/field-type.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { getAnnotation, isLiteralUnion } from '@dxos/effect';
|
|
|
9
9
|
/**
|
|
10
10
|
* Detected field type for a schema property.
|
|
11
11
|
*/
|
|
12
|
-
export type SettingsFieldType = 'boolean' | 'string' | 'number' | 'select';
|
|
12
|
+
export type SettingsFieldType = 'boolean' | 'string' | 'number' | 'select' | 'struct';
|
|
13
13
|
|
|
14
14
|
export type SelectOption = {
|
|
15
15
|
value: string | number;
|
|
@@ -23,6 +23,9 @@ export const detectFieldType = (ast: SchemaAST.AST): SettingsFieldType | undefin
|
|
|
23
23
|
if (isLiteralUnion(ast)) {
|
|
24
24
|
return 'select';
|
|
25
25
|
}
|
|
26
|
+
if (SchemaAST.isTypeLiteral(ast)) {
|
|
27
|
+
return 'struct';
|
|
28
|
+
}
|
|
26
29
|
|
|
27
30
|
switch (ast._tag) {
|
|
28
31
|
case 'BooleanKeyword':
|