@dxos/react-ui-form 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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 +620 -440
- 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 +620 -440
- 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 -62
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +27 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +73 -72
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +5 -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 +10 -4
- 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 -62
- 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.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +17 -3
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -62
- 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 +108 -64
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
- 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/Settings/Settings.d.ts +24 -8
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -62
- 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 +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +63 -62
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.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.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 -62
- 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.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 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +3 -4
- package/src/components/FieldEditor/FieldEditor.tsx +3 -2
- package/src/components/Form/Form.stories.tsx +5 -16
- package/src/components/Form/Form.tsx +30 -7
- package/src/components/Form/FormField.tsx +7 -5
- package/src/components/Form/FormFieldComponent.tsx +3 -0
- package/src/components/Form/FormFieldSet.tsx +1 -3
- package/src/components/Form/fields/ArrayField.tsx +8 -3
- package/src/components/Form/fields/GeoPointField.tsx +2 -1
- 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 +61 -30
- package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -5
- package/src/components/Form/fields/SelectOptionField.tsx +3 -2
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/ObjectForm/ObjectForm.tsx +6 -5
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +5 -2
- package/src/components/ObjectPicker/ObjectPicker.tsx +28 -12
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +321 -22
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +26 -14
- package/src/components/Settings/AUDIT.md +52 -0
- package/src/components/Settings/Settings.stories.tsx +50 -1
- package/src/components/Settings/Settings.tsx +6 -53
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +62 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +17 -18
- package/src/components/ViewEditor/ViewEditor.tsx +4 -3
- package/src/components/testing.tsx +20 -21
- package/src/index.ts +0 -4
- package/src/translations.ts +7 -4
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -0
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
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
|
+
|
|
12
|
+
import { SettingsFieldSet } from './SettingsFieldSet';
|
|
13
|
+
import { SettingsItem, SettingsPanel } from './SettingsItem';
|
|
11
14
|
|
|
12
15
|
const SETTINGS_ROOT_NAME = 'Settings.Root';
|
|
13
16
|
const SETTINGS_VIEWPORT_NAME = 'Settings.Viewport';
|
|
14
17
|
const SETTINGS_SECTION_NAME = 'Settings.Section';
|
|
15
|
-
const SETTINGS_PANEL_NAME = 'Settings.Panel';
|
|
16
|
-
const SETTINGS_ITEM_NAME = 'Settings.Item';
|
|
17
18
|
|
|
18
19
|
//
|
|
19
20
|
// Root (headless — no styling, no scroll).
|
|
@@ -67,55 +68,6 @@ const SettingsSection = ({ title, description, children }: SettingsSectionProps)
|
|
|
67
68
|
|
|
68
69
|
SettingsSection.displayName = SETTINGS_SECTION_NAME;
|
|
69
70
|
|
|
70
|
-
//
|
|
71
|
-
// Panel (bordered card container).
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
|
|
75
|
-
|
|
76
|
-
const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
|
|
77
|
-
return (
|
|
78
|
-
<div role='none' className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>
|
|
79
|
-
{children}
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
SettingsPanel.displayName = SETTINGS_PANEL_NAME;
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
// Item (label + optional description + control, proper label semantics).
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
type SettingsItemProps = PropsWithChildren<{
|
|
91
|
-
title: Label;
|
|
92
|
-
description?: Label;
|
|
93
|
-
}>;
|
|
94
|
-
|
|
95
|
-
const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
|
|
96
|
-
const { t } = useTranslation(translationKey);
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<Input.Root>
|
|
100
|
-
<SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
|
|
101
|
-
<Input.Label classNames='pb-trim-md text-base-surface-text text-lg md:col-span-2'>
|
|
102
|
-
{toLocalizedString(title, t)}
|
|
103
|
-
</Input.Label>
|
|
104
|
-
<Input.DescriptionAndValidation>
|
|
105
|
-
<Input.Description classNames='text-base text-description'>
|
|
106
|
-
{toLocalizedString(description, t)}
|
|
107
|
-
</Input.Description>
|
|
108
|
-
</Input.DescriptionAndValidation>
|
|
109
|
-
<div role='none' className='text-end py-1'>
|
|
110
|
-
{children}
|
|
111
|
-
</div>
|
|
112
|
-
</SettingsPanel>
|
|
113
|
-
</Input.Root>
|
|
114
|
-
);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
SettingsItem.displayName = SETTINGS_ITEM_NAME;
|
|
118
|
-
|
|
119
71
|
//
|
|
120
72
|
// Settings
|
|
121
73
|
//
|
|
@@ -126,4 +78,5 @@ export const Settings = {
|
|
|
126
78
|
Section: SettingsSection,
|
|
127
79
|
Panel: SettingsPanel,
|
|
128
80
|
Item: SettingsItem,
|
|
81
|
+
FieldSet: SettingsFieldSet,
|
|
129
82
|
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
6
|
+
import * as String from 'effect/String';
|
|
7
|
+
import React, { useCallback, useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import { getAnnotation, type SchemaProperty } from '@dxos/effect';
|
|
10
|
+
import { Input, Select } from '@dxos/react-ui';
|
|
11
|
+
|
|
12
|
+
import { getFormProperties, type SelectOption, detectFieldType, getSelectOptionsFromAst } from '../../util';
|
|
13
|
+
import { SettingsItem } from './SettingsItem';
|
|
14
|
+
|
|
15
|
+
//
|
|
16
|
+
// Types
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for a custom field renderer.
|
|
21
|
+
* The renderer provides the control only; SettingsItem wrapper is handled by FieldSet.
|
|
22
|
+
*/
|
|
23
|
+
export type SettingsFieldProps<T = any> = {
|
|
24
|
+
value: T;
|
|
25
|
+
onChange: (value: T) => void;
|
|
26
|
+
readonly?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
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.
|
|
32
|
+
*/
|
|
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
|
+
};
|
|
38
|
+
|
|
39
|
+
export type SettingsFieldSetProps<T extends Record<string, any> = Record<string, any>> = {
|
|
40
|
+
/** Effect Schema for the settings object. */
|
|
41
|
+
schema: { ast: SchemaAST.AST };
|
|
42
|
+
|
|
43
|
+
/** Current settings values. */
|
|
44
|
+
values: T;
|
|
45
|
+
|
|
46
|
+
/** Callback when any value changes. Receives a new complete object. */
|
|
47
|
+
onValuesChanged?: (values: T) => void;
|
|
48
|
+
|
|
49
|
+
/** When true, all controls are disabled. */
|
|
50
|
+
readonly?: boolean;
|
|
51
|
+
|
|
52
|
+
/** Map of property names to custom field renderers. Nested objects map to struct sub-fields. */
|
|
53
|
+
fieldMap?: SettingsFieldMap<T>;
|
|
54
|
+
|
|
55
|
+
/** Control field visibility. Receives the dotted path (e.g. `'modelDefaults.edge'`) at every level. */
|
|
56
|
+
visible?: (path: string, values: T) => boolean;
|
|
57
|
+
|
|
58
|
+
/** Override the order of fields. Fields not listed are appended in schema order. Applied per scope. */
|
|
59
|
+
sort?: string[];
|
|
60
|
+
|
|
61
|
+
/** Internal: dot-path prefix used by recursive struct rendering. */
|
|
62
|
+
pathPrefix?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
//
|
|
66
|
+
// Component
|
|
67
|
+
//
|
|
68
|
+
|
|
69
|
+
export const SettingsFieldSet = <T extends Record<string, any>>({
|
|
70
|
+
schema,
|
|
71
|
+
values,
|
|
72
|
+
onValuesChanged,
|
|
73
|
+
readonly,
|
|
74
|
+
fieldMap,
|
|
75
|
+
visible,
|
|
76
|
+
sort,
|
|
77
|
+
pathPrefix = '',
|
|
78
|
+
}: SettingsFieldSetProps<T>) => {
|
|
79
|
+
const properties = useMemo(() => {
|
|
80
|
+
const props = getFormProperties(schema.ast);
|
|
81
|
+
if (!sort) {
|
|
82
|
+
return props;
|
|
83
|
+
}
|
|
84
|
+
const rank = (name: string) => {
|
|
85
|
+
const idx = sort.indexOf(name);
|
|
86
|
+
return idx === -1 ? Number.POSITIVE_INFINITY : idx;
|
|
87
|
+
};
|
|
88
|
+
return [...props].sort(({ name: a }, { name: b }) => rank(a.toString()) - rank(b.toString()));
|
|
89
|
+
}, [schema, sort]);
|
|
90
|
+
|
|
91
|
+
const handleChange = useCallback(
|
|
92
|
+
(name: string, value: unknown) => {
|
|
93
|
+
onValuesChanged?.({ ...values, [name]: value } as T);
|
|
94
|
+
},
|
|
95
|
+
[values, onValuesChanged],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
{properties.map((property) => {
|
|
101
|
+
const name = property.name.toString();
|
|
102
|
+
const path = pathPrefix ? `${pathPrefix}.${name}` : name;
|
|
103
|
+
if (visible && !visible(path, values)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const entry = fieldMap?.[name as keyof typeof fieldMap] as
|
|
108
|
+
| React.FC<SettingsFieldProps>
|
|
109
|
+
| SettingsFieldMap
|
|
110
|
+
| undefined;
|
|
111
|
+
return (
|
|
112
|
+
<SettingsFieldItem
|
|
113
|
+
key={name}
|
|
114
|
+
path={path}
|
|
115
|
+
property={property}
|
|
116
|
+
value={values[name]}
|
|
117
|
+
onChange={(value) => handleChange(name, value)}
|
|
118
|
+
readonly={readonly}
|
|
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}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
SettingsFieldSet.displayName = 'Settings.FieldSet';
|
|
131
|
+
|
|
132
|
+
//
|
|
133
|
+
// Internal: render a single settings field.
|
|
134
|
+
//
|
|
135
|
+
|
|
136
|
+
type SettingsFieldItemProps = {
|
|
137
|
+
property: SchemaProperty;
|
|
138
|
+
path: string;
|
|
139
|
+
value: any;
|
|
140
|
+
onChange: (value: any) => void;
|
|
141
|
+
readonly?: boolean;
|
|
142
|
+
customField?: React.FC<SettingsFieldProps>;
|
|
143
|
+
nestedFieldMap?: SettingsFieldMap;
|
|
144
|
+
visible?: (path: string, values: any) => boolean;
|
|
145
|
+
sort?: string[];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const SettingsFieldItem = ({
|
|
149
|
+
property,
|
|
150
|
+
path,
|
|
151
|
+
value,
|
|
152
|
+
onChange,
|
|
153
|
+
readonly,
|
|
154
|
+
customField,
|
|
155
|
+
nestedFieldMap,
|
|
156
|
+
visible,
|
|
157
|
+
sort,
|
|
158
|
+
}: SettingsFieldItemProps) => {
|
|
159
|
+
const { type } = property;
|
|
160
|
+
const name = property.name.toString();
|
|
161
|
+
const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type) ?? String.capitalize(name);
|
|
162
|
+
const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
|
|
163
|
+
|
|
164
|
+
// Custom field renderer.
|
|
165
|
+
if (customField) {
|
|
166
|
+
const CustomField = customField;
|
|
167
|
+
return (
|
|
168
|
+
<SettingsItem title={title} description={description}>
|
|
169
|
+
<CustomField value={value} onChange={onChange} readonly={readonly} />
|
|
170
|
+
</SettingsItem>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fieldType = detectFieldType(type);
|
|
175
|
+
|
|
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
|
+
|
|
191
|
+
case 'boolean':
|
|
192
|
+
return (
|
|
193
|
+
<SettingsItem title={title} description={description}>
|
|
194
|
+
<Input.Switch disabled={readonly} checked={!!value} onCheckedChange={(checked) => onChange(!!checked)} />
|
|
195
|
+
</SettingsItem>
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
case 'select': {
|
|
199
|
+
const options = getSelectOptionsFromAst(type) ?? [];
|
|
200
|
+
return (
|
|
201
|
+
<SettingsItem title={title} description={description}>
|
|
202
|
+
<Select.Root
|
|
203
|
+
disabled={readonly}
|
|
204
|
+
value={value == null ? '' : globalThis.String(value)}
|
|
205
|
+
onValueChange={(selected) => {
|
|
206
|
+
const matched = options.find((option) => globalThis.String(option.value) === selected);
|
|
207
|
+
onChange(matched?.value ?? selected);
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<Select.TriggerButton disabled={readonly} />
|
|
211
|
+
<Select.Portal>
|
|
212
|
+
<Select.Content>
|
|
213
|
+
<Select.Viewport>
|
|
214
|
+
{options.map((option: SelectOption) => (
|
|
215
|
+
<Select.Option key={globalThis.String(option.value)} value={globalThis.String(option.value)}>
|
|
216
|
+
{option.label ?? globalThis.String(option.value)}
|
|
217
|
+
</Select.Option>
|
|
218
|
+
))}
|
|
219
|
+
</Select.Viewport>
|
|
220
|
+
<Select.Arrow />
|
|
221
|
+
</Select.Content>
|
|
222
|
+
</Select.Portal>
|
|
223
|
+
</Select.Root>
|
|
224
|
+
</SettingsItem>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'string':
|
|
229
|
+
return (
|
|
230
|
+
<SettingsItem title={title} description={description}>
|
|
231
|
+
<Input.TextInput disabled={readonly} value={value ?? ''} onChange={(event) => onChange(event.target.value)} />
|
|
232
|
+
</SettingsItem>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
case 'number':
|
|
236
|
+
return (
|
|
237
|
+
<SettingsItem title={title} description={description}>
|
|
238
|
+
<Input.TextInput
|
|
239
|
+
disabled={readonly}
|
|
240
|
+
type='number'
|
|
241
|
+
value={value ?? ''}
|
|
242
|
+
onChange={(event) => {
|
|
243
|
+
if (event.target.value === '') {
|
|
244
|
+
onChange(undefined);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const parsed = Number(event.target.value);
|
|
248
|
+
onChange(Number.isNaN(parsed) ? undefined : parsed);
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
</SettingsItem>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
default:
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type Label, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/ui-theme';
|
|
9
|
+
|
|
10
|
+
import { translationKey } from '#translations';
|
|
11
|
+
|
|
12
|
+
const SETTINGS_PANEL_NAME = 'Settings.Panel';
|
|
13
|
+
const SETTINGS_ITEM_NAME = 'Settings.Item';
|
|
14
|
+
|
|
15
|
+
//
|
|
16
|
+
// Panel (bordered card container).
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
|
|
20
|
+
|
|
21
|
+
export const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
|
|
22
|
+
return (
|
|
23
|
+
<div role='none' className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
SettingsPanel.displayName = SETTINGS_PANEL_NAME;
|
|
30
|
+
|
|
31
|
+
//
|
|
32
|
+
// Item (label + optional description + control, proper label semantics).
|
|
33
|
+
//
|
|
34
|
+
|
|
35
|
+
type SettingsItemProps = PropsWithChildren<{
|
|
36
|
+
title: Label;
|
|
37
|
+
description?: Label;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
export const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
|
|
41
|
+
const { t } = useTranslation(translationKey);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Input.Root>
|
|
45
|
+
<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-surface-text text-lg md:col-span-2'>
|
|
47
|
+
{toLocalizedString(title, t)}
|
|
48
|
+
</Input.Label>
|
|
49
|
+
<Input.DescriptionAndValidation>
|
|
50
|
+
<Input.Description classNames='text-base text-description'>
|
|
51
|
+
{toLocalizedString(description, t)}
|
|
52
|
+
</Input.Description>
|
|
53
|
+
</Input.DescriptionAndValidation>
|
|
54
|
+
<div role='none' className='text-end py-1'>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
</SettingsPanel>
|
|
58
|
+
</Input.Root>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
SettingsItem.displayName = SETTINGS_ITEM_NAME;
|
|
@@ -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
|
+
};
|
|
@@ -16,7 +16,8 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
|
16
16
|
import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
|
|
17
17
|
import { Employer, Organization, Person, Pipeline } from '@dxos/types';
|
|
18
18
|
|
|
19
|
-
import { translations } from '
|
|
19
|
+
import { translations } from '#translations';
|
|
20
|
+
|
|
20
21
|
import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
21
22
|
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
22
23
|
|
|
@@ -90,7 +91,7 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
90
91
|
if (props.mode === 'tag') {
|
|
91
92
|
const queue = target && DXN.tryParse(target) ? target : undefined;
|
|
92
93
|
const query = queue ? Query.fromAst(newQuery).from({ queues: [queue] }) : Query.fromAst(newQuery);
|
|
93
|
-
Obj.
|
|
94
|
+
Obj.update(view, (view) => {
|
|
94
95
|
view.query.ast = query.ast as Mutable<typeof query.ast>;
|
|
95
96
|
});
|
|
96
97
|
|
|
@@ -104,12 +105,12 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
104
105
|
query,
|
|
105
106
|
jsonSchema: newSchema.jsonSchema,
|
|
106
107
|
});
|
|
107
|
-
Obj.
|
|
108
|
+
Obj.update(view, (view) => {
|
|
108
109
|
view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
|
|
109
110
|
});
|
|
110
111
|
setSchema(() => newSchema);
|
|
111
112
|
} else {
|
|
112
|
-
Obj.
|
|
113
|
+
Obj.update(view, (view) => {
|
|
113
114
|
view.query.ast = newQuery as Mutable<typeof newQuery>;
|
|
114
115
|
});
|
|
115
116
|
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
@@ -146,20 +147,18 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
146
147
|
|
|
147
148
|
return (
|
|
148
149
|
<TestLayout json={json}>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
/>
|
|
162
|
-
</div>
|
|
150
|
+
<ViewEditor
|
|
151
|
+
ref={projectionRef}
|
|
152
|
+
schema={schema}
|
|
153
|
+
view={view}
|
|
154
|
+
registry={space?.db.schemaRegistry}
|
|
155
|
+
mode={props.mode}
|
|
156
|
+
readonly={props.readonly}
|
|
157
|
+
types={types}
|
|
158
|
+
tags={tags}
|
|
159
|
+
onQueryChanged={updateViewQuery}
|
|
160
|
+
onDelete={handleDelete}
|
|
161
|
+
/>
|
|
163
162
|
</TestLayout>
|
|
164
163
|
);
|
|
165
164
|
};
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
} from '@dxos/schema';
|
|
27
27
|
import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
|
|
28
28
|
|
|
29
|
-
import { translationKey } from '
|
|
29
|
+
import { translationKey } from '#translations';
|
|
30
|
+
|
|
30
31
|
import { FieldEditor } from '../FieldEditor';
|
|
31
32
|
import {
|
|
32
33
|
Form,
|
|
@@ -274,7 +275,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
274
275
|
const handleMove = useCallback(
|
|
275
276
|
(fromIndex: number, toIndex: number) => {
|
|
276
277
|
invariant(!readonly);
|
|
277
|
-
Obj.
|
|
278
|
+
Obj.update(view, (view) => {
|
|
278
279
|
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
279
280
|
const fields = [...view.projection.fields];
|
|
280
281
|
const [moved] = fields.splice(fromIndex, 1);
|
|
@@ -341,7 +342,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
341
342
|
{field.path}
|
|
342
343
|
</List.ItemTitle>
|
|
343
344
|
<List.ItemIconButton
|
|
344
|
-
label={t(hidden ? 'show
|
|
345
|
+
label={t(hidden ? 'show-field.label' : 'hide-field.label')}
|
|
345
346
|
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
346
347
|
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
347
348
|
autoHide={false}
|
|
@@ -4,27 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { Syntax } from '@dxos/react-ui-syntax-highlighter';
|
|
9
|
+
import { composableProps, slottable } from '@dxos/ui-theme';
|
|
10
10
|
|
|
11
|
-
type TestLayoutProps =
|
|
11
|
+
type TestLayoutProps = PropsWithChildren<{ json?: any }>;
|
|
12
12
|
|
|
13
|
-
export const TestLayout = ({
|
|
13
|
+
export const TestLayout = ({ children, json }: TestLayoutProps) => {
|
|
14
14
|
return (
|
|
15
|
-
<div className='
|
|
16
|
-
<TestPanel
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</Json.Content>
|
|
27
|
-
</Json.Root>
|
|
15
|
+
<div role='none' className='dx-container grid grid-cols-[1fr_1fr] p-4 gap-4'>
|
|
16
|
+
<TestPanel>{children}</TestPanel>
|
|
17
|
+
<TestPanel>
|
|
18
|
+
<Syntax.Root data={json}>
|
|
19
|
+
<Syntax.Content>
|
|
20
|
+
<Syntax.Filter />
|
|
21
|
+
<Syntax.Viewport>
|
|
22
|
+
<Syntax.Code testId='debug' classNames='text-xs' />
|
|
23
|
+
</Syntax.Viewport>
|
|
24
|
+
</Syntax.Content>
|
|
25
|
+
</Syntax.Root>
|
|
28
26
|
</TestPanel>
|
|
29
27
|
</div>
|
|
30
28
|
);
|
|
@@ -32,14 +30,15 @@ export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
|
32
30
|
|
|
33
31
|
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
34
32
|
|
|
35
|
-
const TestPanel = ({
|
|
33
|
+
const TestPanel = slottable<HTMLDivElement, TestPanelProps>(({ children }, forwardedRef) => {
|
|
36
34
|
return (
|
|
37
|
-
<div
|
|
35
|
+
<div {...composableProps({ classNames: 'dx-container bg-modal-surface rounded-md' })} ref={forwardedRef}>
|
|
38
36
|
{children}
|
|
39
37
|
</div>
|
|
40
38
|
);
|
|
41
|
-
};
|
|
39
|
+
});
|
|
42
40
|
|
|
43
41
|
// Symbol for accessing debug objects in tests.
|
|
44
42
|
export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
|
|
45
43
|
export const FIELD_EDITOR_DEBUG_SYMBOL = Symbol('fieldEditorDebug');
|
|
44
|
+
export const OBJECT_PROPERTIES_DEBUG_SYMBOL = Symbol('objectPropertiesDebug');
|
package/src/index.ts
CHANGED
package/src/translations.ts
CHANGED
|
@@ -14,23 +14,25 @@ export const translations = [
|
|
|
14
14
|
'hidden-fields.label': 'Hidden Fields',
|
|
15
15
|
'empty-readonly-ref-field.label': '(none)',
|
|
16
16
|
|
|
17
|
-
'add-property-button.label': 'Add property',
|
|
18
17
|
'field-property.label': 'Property',
|
|
19
18
|
'field-property.placeholder': 'Property name',
|
|
20
19
|
'field-format.label': 'Type',
|
|
21
20
|
'field-path.label': 'Field path',
|
|
22
|
-
|
|
21
|
+
|
|
22
|
+
'add-item.button': 'Add item',
|
|
23
|
+
'add-property-button.label': 'Add property',
|
|
23
24
|
'boolean-input-true.value': 'Yes',
|
|
24
25
|
'boolean-input-false.value': 'No',
|
|
25
26
|
'show-field.label': 'Show field',
|
|
26
27
|
'hide-field.label': 'Hide field',
|
|
27
28
|
'delete-field.label': 'Delete field',
|
|
29
|
+
'create-option.label': 'Create',
|
|
30
|
+
'add-tag.label': 'Add tag',
|
|
31
|
+
|
|
28
32
|
'ref-field-combobox-input.placeholder': 'Search…',
|
|
29
33
|
'ref-field.placeholder': 'Select…',
|
|
30
34
|
'ref-field.placeholder_one': 'Select…',
|
|
31
35
|
'ref-field.placeholder_other': 'Select items…',
|
|
32
|
-
'create-option.label': 'Create',
|
|
33
|
-
'add-tag.label': 'Add tag',
|
|
34
36
|
|
|
35
37
|
'example.placeholder': 'Example',
|
|
36
38
|
'latitude.placeholder': 'Latitude (e.g., 40.7128)',
|
|
@@ -38,6 +40,7 @@ export const translations = [
|
|
|
38
40
|
|
|
39
41
|
'cancel-button.label': 'Cancel',
|
|
40
42
|
'save-button.label': 'Save',
|
|
43
|
+
'create-text.label': 'Create text',
|
|
41
44
|
|
|
42
45
|
// TypeFormat
|
|
43
46
|
'format.boolean.label': 'Boolean',
|