@dxos/react-ui-form 0.8.4-main.c1de068 → 0.8.4-main.dedc0f3
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 +601 -528
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +601 -528
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +79 -4
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +74 -4
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +5 -5
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +5 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +126 -30
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +16 -10
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +4 -1
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +10 -4
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +74 -3
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +76 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +6 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +1 -1
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +9 -4
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -30
- package/src/components/ControlSection/ControlSection.stories.tsx +8 -10
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +8 -9
- package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
- package/src/components/FieldEditor/FieldEditor.tsx +22 -17
- package/src/components/Form/ArrayField.tsx +8 -8
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +129 -93
- package/src/components/Form/Form.tsx +16 -12
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +96 -34
- package/src/components/Form/FormContext.tsx +5 -5
- package/src/components/Form/Input.tsx +4 -1
- package/src/components/Form/RefField.tsx +126 -40
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +7 -6
- package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +50 -23
- package/src/components/ViewEditor/ViewEditor.test.tsx +7 -10
- package/src/components/ViewEditor/ViewEditor.tsx +151 -132
- package/src/components/index.ts +0 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +5 -3
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/translations.ts +11 -5
- package/src/util/field.ts +8 -18
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/src/components/deprecated/Form.stories.tsx +0 -62
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
|
@@ -19,9 +19,12 @@ export type InputProps = {
|
|
|
19
19
|
type: SimpleType;
|
|
20
20
|
format?: FormatEnum;
|
|
21
21
|
label: string;
|
|
22
|
-
disabled?: boolean;
|
|
23
22
|
placeholder?: string;
|
|
24
23
|
inputOnly?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Specifies the readonly variant: either disabled inputs, elements indicating they are usually editable but currently are not; or `static`, a field’s representation as regular content without signifiers that it is ever editable.
|
|
26
|
+
*/
|
|
27
|
+
readonly?: 'disabled-input' | 'static' | false;
|
|
25
28
|
} & FormInputStateProps;
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -3,38 +3,50 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type SchemaAST } from 'effect';
|
|
6
|
-
import React, { useCallback, useMemo,
|
|
6
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
Expando,
|
|
10
|
-
getTypeAnnotation,
|
|
11
10
|
Ref,
|
|
12
11
|
ReferenceAnnotationId,
|
|
13
12
|
type ReferenceAnnotationValue,
|
|
13
|
+
type TypeAnnotation,
|
|
14
|
+
getTypeAnnotation,
|
|
14
15
|
} from '@dxos/echo-schema';
|
|
15
16
|
import { findAnnotation } from '@dxos/effect';
|
|
16
17
|
import { DXN } from '@dxos/keys';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { type DxTagPickerItemClick } from '@dxos/lit-ui';
|
|
19
|
+
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
20
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
21
|
+
import { PopoverCombobox } from '@dxos/react-ui-searchlist';
|
|
22
|
+
import { TagPickerItem } from '@dxos/react-ui-tag-picker';
|
|
20
23
|
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
21
|
-
import { isNonNullable } from '@dxos/util';
|
|
24
|
+
import { type MaybePromise, isNonNullable } from '@dxos/util';
|
|
22
25
|
|
|
23
|
-
import { TextInput } from './Defaults';
|
|
24
|
-
import { InputHeader, type InputProps } from './Input';
|
|
25
26
|
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
26
27
|
import { translationKey } from '../../translations';
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
import { TextInput } from './Defaults';
|
|
30
|
+
import { InputHeader, type InputProps } from './Input';
|
|
31
|
+
|
|
32
|
+
export type RefFieldProps = InputProps & {
|
|
29
33
|
ast?: SchemaAST.AST;
|
|
30
34
|
array?: boolean;
|
|
31
35
|
onQueryRefOptions?: QueryRefOptions;
|
|
36
|
+
createOptionLabel?: [string, { ns: string }];
|
|
37
|
+
createOptionIcon?: string;
|
|
38
|
+
onCreateFromQuery?: (type: TypeAnnotation, query: string) => MaybePromise<void>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// TODO(thure): Is this a standard that should be better canonized?
|
|
42
|
+
const isRefSnapShot = (val: any): val is { '/': string } => {
|
|
43
|
+
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
32
44
|
};
|
|
33
45
|
|
|
34
46
|
export const RefField = ({
|
|
35
47
|
type,
|
|
36
48
|
label,
|
|
37
|
-
|
|
49
|
+
readonly,
|
|
38
50
|
placeholder,
|
|
39
51
|
inputOnly,
|
|
40
52
|
array,
|
|
@@ -42,6 +54,9 @@ export const RefField = ({
|
|
|
42
54
|
getValue,
|
|
43
55
|
onBlur,
|
|
44
56
|
onQueryRefOptions,
|
|
57
|
+
createOptionLabel,
|
|
58
|
+
createOptionIcon,
|
|
59
|
+
onCreateFromQuery,
|
|
45
60
|
onValueChange,
|
|
46
61
|
...restInputProps
|
|
47
62
|
}: RefFieldProps) => {
|
|
@@ -56,17 +71,18 @@ export const RefField = ({
|
|
|
56
71
|
// If ref type is expando, fall back to taking a DXN in string format.
|
|
57
72
|
return (
|
|
58
73
|
<RefFieldFallback
|
|
59
|
-
{...{ type, label, placeholder,
|
|
74
|
+
{...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
60
75
|
/>
|
|
61
76
|
);
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
const handleGetValue = useCallback(()
|
|
79
|
+
const handleGetValue = useCallback(() => {
|
|
65
80
|
const formValue = getValue();
|
|
66
81
|
|
|
67
82
|
const unknownToRefOption = (val: unknown) => {
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
const isRef = Ref.isRef(val);
|
|
84
|
+
if (isRef || isRefSnapShot(val)) {
|
|
85
|
+
const dxnString = isRef ? val.dxn.toString() : val['/'];
|
|
70
86
|
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
71
87
|
if (matchingOption) {
|
|
72
88
|
return matchingOption;
|
|
@@ -87,15 +103,6 @@ export const RefField = ({
|
|
|
87
103
|
return [];
|
|
88
104
|
}, [getValue, availableOptions, array]);
|
|
89
105
|
|
|
90
|
-
const handleSearch = useCallback(
|
|
91
|
-
(text: string, ids: string[]): TagPickerItemData[] =>
|
|
92
|
-
availableOptions
|
|
93
|
-
.filter((option) => !ids.includes(option.id))
|
|
94
|
-
.filter((option) => option.label.toLowerCase().includes(text.toLowerCase()))
|
|
95
|
-
.map((option) => ({ id: option.id, label: option.label, hue: option.hue as any })),
|
|
96
|
-
[availableOptions],
|
|
97
|
-
);
|
|
98
|
-
|
|
99
106
|
const handleUpdate = useCallback(
|
|
100
107
|
(ids: string[]) => {
|
|
101
108
|
if (ids.length === 0) {
|
|
@@ -123,8 +130,6 @@ export const RefField = ({
|
|
|
123
130
|
[availableOptions, array, onValueChange],
|
|
124
131
|
);
|
|
125
132
|
|
|
126
|
-
const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
|
|
127
|
-
|
|
128
133
|
if (!refTypeInfo) {
|
|
129
134
|
return null;
|
|
130
135
|
}
|
|
@@ -132,32 +137,113 @@ export const RefField = ({
|
|
|
132
137
|
const { status, error } = restInputProps.getStatus();
|
|
133
138
|
|
|
134
139
|
const items = handleGetValue();
|
|
140
|
+
const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
|
|
141
|
+
const labelById = useMemo(
|
|
142
|
+
() =>
|
|
143
|
+
availableOptions.reduce((acc: Record<string, string>, option) => {
|
|
144
|
+
acc[option.id.toLowerCase()] = option.label.toLowerCase();
|
|
145
|
+
return acc;
|
|
146
|
+
}, {}),
|
|
147
|
+
[availableOptions],
|
|
148
|
+
);
|
|
149
|
+
const [query, setQuery] = useState('');
|
|
150
|
+
const toggleSelect = useCallback(
|
|
151
|
+
(id: string) => {
|
|
152
|
+
if (array) {
|
|
153
|
+
const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
|
|
154
|
+
handleUpdate(nextIds);
|
|
155
|
+
} else {
|
|
156
|
+
if (selectedIds[0] === id) {
|
|
157
|
+
handleUpdate([]);
|
|
158
|
+
} else {
|
|
159
|
+
handleUpdate([id]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[array, selectedIds, handleUpdate],
|
|
164
|
+
);
|
|
135
165
|
|
|
136
166
|
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
137
|
-
return
|
|
167
|
+
return readonly && items.length < 1 ? null : (
|
|
138
168
|
<Input.Root validationValence={status}>
|
|
139
169
|
{!inputOnly && <InputHeader error={error} label={label} />}
|
|
140
170
|
<div data-no-submit>
|
|
141
|
-
{
|
|
171
|
+
{readonly ? (
|
|
142
172
|
items.length < 1 ? (
|
|
143
173
|
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
144
174
|
) : (
|
|
145
175
|
items.map((item) => (
|
|
146
|
-
<
|
|
176
|
+
<DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
|
|
147
177
|
{item.label}
|
|
148
|
-
</
|
|
178
|
+
</DxAnchor>
|
|
149
179
|
))
|
|
150
180
|
)
|
|
151
181
|
) : (
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
<PopoverCombobox.Root>
|
|
183
|
+
<PopoverCombobox.Trigger asChild>
|
|
184
|
+
<Button variant='ghost' classNames='is-full text-start gap-2'>
|
|
185
|
+
<div role='none' className='grow'>
|
|
186
|
+
{items?.length ? (
|
|
187
|
+
items?.map((item) => (
|
|
188
|
+
<TagPickerItem
|
|
189
|
+
key={item.id}
|
|
190
|
+
itemId={item.id}
|
|
191
|
+
label={item.label}
|
|
192
|
+
{...(item.hue ? { hue: item.hue } : {})}
|
|
193
|
+
removeLabel={t('remove item label')}
|
|
194
|
+
onItemClick={(event: DxTagPickerItemClick) => {
|
|
195
|
+
if (event.action === 'remove') {
|
|
196
|
+
toggleSelect(item.id);
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
/>
|
|
200
|
+
))
|
|
201
|
+
) : (
|
|
202
|
+
<span className='text-description'>
|
|
203
|
+
{placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
<Icon size={3} icon='ph--caret-down--bold' />
|
|
208
|
+
</Button>
|
|
209
|
+
</PopoverCombobox.Trigger>
|
|
210
|
+
<PopoverCombobox.Content
|
|
211
|
+
filter={(value, search) =>
|
|
212
|
+
value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
<PopoverCombobox.Input
|
|
216
|
+
placeholder={'Search...'}
|
|
217
|
+
value={query}
|
|
218
|
+
onValueChange={(v) => setQuery(v)}
|
|
219
|
+
autoFocus
|
|
220
|
+
/>
|
|
221
|
+
<PopoverCombobox.List>
|
|
222
|
+
{availableOptions.map((option) => (
|
|
223
|
+
<PopoverCombobox.Item
|
|
224
|
+
key={option.id}
|
|
225
|
+
value={option.id}
|
|
226
|
+
onSelect={() => toggleSelect(option.id)}
|
|
227
|
+
classNames='flex items-center gap-2'
|
|
228
|
+
>
|
|
229
|
+
<span className='grow'>{option.label}</span>
|
|
230
|
+
{selectedIds.includes(option.id) && <Icon icon='ph--check--regular' />}
|
|
231
|
+
</PopoverCombobox.Item>
|
|
232
|
+
))}
|
|
233
|
+
{query.length > 0 && createOptionLabel && createOptionIcon && onCreateFromQuery && (
|
|
234
|
+
<PopoverCombobox.Item
|
|
235
|
+
value='__create__'
|
|
236
|
+
onSelect={() => onCreateFromQuery?.(refTypeInfo, query)}
|
|
237
|
+
classNames='flex items-center gap-2'
|
|
238
|
+
>
|
|
239
|
+
<Icon icon={createOptionIcon} />
|
|
240
|
+
{t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })}
|
|
241
|
+
</PopoverCombobox.Item>
|
|
242
|
+
)}
|
|
243
|
+
</PopoverCombobox.List>
|
|
244
|
+
<PopoverCombobox.Arrow />
|
|
245
|
+
</PopoverCombobox.Content>
|
|
246
|
+
</PopoverCombobox.Root>
|
|
161
247
|
)}
|
|
162
248
|
</div>
|
|
163
249
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
@@ -168,7 +254,7 @@ export const RefField = ({
|
|
|
168
254
|
const RefFieldFallback = ({
|
|
169
255
|
type,
|
|
170
256
|
label,
|
|
171
|
-
|
|
257
|
+
readonly,
|
|
172
258
|
placeholder,
|
|
173
259
|
inputOnly,
|
|
174
260
|
getValue,
|
|
@@ -202,7 +288,7 @@ const RefFieldFallback = ({
|
|
|
202
288
|
<TextInput
|
|
203
289
|
type={type}
|
|
204
290
|
label={label}
|
|
205
|
-
|
|
291
|
+
readonly={readonly}
|
|
206
292
|
placeholder={placeholder}
|
|
207
293
|
inputOnly={inputOnly}
|
|
208
294
|
getValue={handleGetValue as <V>() => V | undefined}
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type GeoPoint
|
|
7
|
+
import { GeoLocation, type GeoPoint } from '@dxos/echo-schema';
|
|
8
8
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
9
|
import { safeParseFloat } from '@dxos/util';
|
|
10
10
|
|
|
11
11
|
import { translationKey } from '../../../translations';
|
|
12
12
|
import { InputHeader, type InputProps } from '../Input';
|
|
13
13
|
|
|
14
|
-
export const GeoPointInput = ({ type, label,
|
|
14
|
+
export const GeoPointInput = ({ type, label, readonly, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
|
|
15
15
|
const { t } = useTranslation(translationKey);
|
|
16
16
|
const { status, error } = getStatus();
|
|
17
17
|
const geoPoint = useMemo<GeoPoint>(() => getValue<GeoPoint>() ?? [0, 0], [getValue]);
|
|
@@ -48,24 +48,32 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
|
|
|
48
48
|
<Input.Root validationValence={status}>
|
|
49
49
|
<InputHeader error={error} label={label} />
|
|
50
50
|
<div role='none' className='grid grid-cols-2 gap-2'>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
51
|
+
<div role='none'>
|
|
52
|
+
<Input.Label>{t('latitude label')}</Input.Label>
|
|
53
|
+
<Input.TextInput
|
|
54
|
+
type='number'
|
|
55
|
+
step='0.00001'
|
|
56
|
+
min='-90'
|
|
57
|
+
max='90'
|
|
58
|
+
disabled={!!readonly}
|
|
59
|
+
value={latitudeText}
|
|
60
|
+
onChange={handleCoordinateChange('latitude', setLatitudeText)}
|
|
61
|
+
onBlur={onBlur}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div role='none'>
|
|
65
|
+
<Input.Label>{t('longitude label')}</Input.Label>
|
|
66
|
+
<Input.TextInput
|
|
67
|
+
type='number'
|
|
68
|
+
step='0.00001'
|
|
69
|
+
min='-180'
|
|
70
|
+
max='180'
|
|
71
|
+
disabled={!!readonly}
|
|
72
|
+
value={longitudeText}
|
|
73
|
+
onChange={handleCoordinateChange('longitude', setLongitudeText)}
|
|
74
|
+
onBlur={onBlur}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
69
77
|
</div>
|
|
70
78
|
</Input.Root>
|
|
71
79
|
);
|
|
@@ -8,14 +8,15 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
8
8
|
import { Schema } from 'effect';
|
|
9
9
|
import React, { useState } from 'react';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type SelectOption, SelectOptionSchema } from '@dxos/echo-schema';
|
|
12
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
13
13
|
|
|
14
|
-
import { SelectOptionInput } from './SelectOptionsInput';
|
|
15
14
|
import { translations } from '../../../translations';
|
|
16
15
|
import { TestLayout, TestPanel } from '../../testing';
|
|
17
16
|
import { Form } from '../Form';
|
|
18
17
|
|
|
18
|
+
import { SelectOptionInput } from './SelectOptionsInput';
|
|
19
|
+
|
|
19
20
|
const schema = Schema.Struct({
|
|
20
21
|
options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
|
|
21
22
|
}).pipe(Schema.mutable);
|
|
@@ -44,18 +45,18 @@ const DefaultStory = () => {
|
|
|
44
45
|
);
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
const meta
|
|
48
|
+
const meta = {
|
|
48
49
|
title: 'ui/react-ui-form/SelectOptionsInput',
|
|
49
|
-
component: SelectOptionInput,
|
|
50
|
+
component: SelectOptionInput as any,
|
|
50
51
|
render: DefaultStory,
|
|
51
52
|
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
52
53
|
parameters: {
|
|
53
54
|
translations,
|
|
54
55
|
},
|
|
55
|
-
}
|
|
56
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
56
57
|
|
|
57
58
|
export default meta;
|
|
58
59
|
|
|
59
|
-
type Story = StoryObj
|
|
60
|
+
type Story = StoryObj<typeof meta>;
|
|
60
61
|
|
|
61
62
|
export const Default: Story = {};
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect,
|
|
5
|
+
import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type SelectOption } from '@dxos/echo-schema';
|
|
8
8
|
import { PublicKey } from '@dxos/keys';
|
|
9
|
-
import { type ChromaticPalette,
|
|
9
|
+
import { type ChromaticPalette, IconButton, Input, Tag, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { List } from '@dxos/react-ui-list';
|
|
11
11
|
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
12
|
-
import { hues,
|
|
12
|
+
import { hues, subtleHover } from '@dxos/react-ui-theme';
|
|
13
13
|
|
|
14
14
|
import { translationKey } from '../../../translations';
|
|
15
15
|
import { InputHeader, type InputProps } from '../Input';
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue, onValueChange }: InputProps) => {
|
|
17
|
+
export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue, onValueChange }: InputProps) => {
|
|
19
18
|
const [selected, setSelectedId] = useState<string | null>(null);
|
|
20
19
|
const [isNewOption, setIsNewOption] = useState(false);
|
|
21
20
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
@@ -115,7 +114,12 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
115
114
|
<InputHeader error={error} label={label} />
|
|
116
115
|
<div role='none'>
|
|
117
116
|
{options && (
|
|
118
|
-
<List.Root
|
|
117
|
+
<List.Root
|
|
118
|
+
items={options}
|
|
119
|
+
isItem={(item) => true}
|
|
120
|
+
onMove={readonly ? undefined : handleMove}
|
|
121
|
+
readonly={!!readonly}
|
|
122
|
+
>
|
|
119
123
|
{({ items }) => (
|
|
120
124
|
<div role='list' className='w-full overflow-auto'>
|
|
121
125
|
{items?.map((item) => (
|
|
@@ -123,26 +127,30 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
123
127
|
<List.Item
|
|
124
128
|
role='button'
|
|
125
129
|
item={item}
|
|
126
|
-
classNames={[
|
|
130
|
+
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
|
|
127
131
|
aria-expanded={selected === item.id}
|
|
128
132
|
>
|
|
129
133
|
<div className='flex items-center'>
|
|
130
|
-
<List.ItemDragHandle />
|
|
134
|
+
<List.ItemDragHandle disabled={!!readonly} />
|
|
131
135
|
<List.ItemTitle onClick={() => handleClick(item.id)} classNames='flex-1'>
|
|
132
136
|
{/* TODO(ZaymonFC): Move spacer into Tag component. */}
|
|
133
137
|
<Tag palette={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
|
|
134
138
|
</List.ItemTitle>
|
|
135
|
-
<
|
|
139
|
+
<IconButton
|
|
140
|
+
iconOnly
|
|
141
|
+
variant='ghost'
|
|
142
|
+
label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: 'os' })}
|
|
136
143
|
icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
137
144
|
onClick={() => handleClick(item.id)}
|
|
138
145
|
/>
|
|
139
146
|
</div>
|
|
140
147
|
{selected === item.id && (
|
|
141
|
-
<div className='
|
|
148
|
+
<div className='pli-cardSpacingInline plb-cardSpacingBlock flex flex-col gap-1 density-fine'>
|
|
142
149
|
{/* 16px to match drag handle width. */}
|
|
143
150
|
<Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
|
|
144
|
-
<div className='
|
|
151
|
+
<div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
|
|
145
152
|
<Input.TextInput
|
|
153
|
+
disabled={!!readonly}
|
|
146
154
|
placeholder={t('select option label placeholder')}
|
|
147
155
|
ref={selected === item.id ? inputRef : undefined}
|
|
148
156
|
value={item.title}
|
|
@@ -151,14 +159,9 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
151
159
|
classNames='flex-1'
|
|
152
160
|
data-no-submit
|
|
153
161
|
/>
|
|
154
|
-
<
|
|
155
|
-
<HuePicker
|
|
156
|
-
value={item.color}
|
|
157
|
-
onChange={handleColorChange(item.id)}
|
|
158
|
-
rootVariant='toolbar-button'
|
|
159
|
-
/>
|
|
160
|
-
</Toolbar.Root>
|
|
162
|
+
<HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
|
|
161
163
|
<IconButton
|
|
164
|
+
disabled={!!readonly}
|
|
162
165
|
icon='ph--trash--fill'
|
|
163
166
|
iconOnly
|
|
164
167
|
label={t('select option delete')}
|
|
@@ -170,18 +173,14 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
|
|
|
170
173
|
</List.Item>
|
|
171
174
|
</div>
|
|
172
175
|
))}
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<span className='text-sm'>{t('select option add')}</span>
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
</div>
|
|
176
|
+
<IconButton
|
|
177
|
+
variant='ghost'
|
|
178
|
+
icon='ph--plus--regular'
|
|
179
|
+
label={t('select option add')}
|
|
180
|
+
onClick={handleAdd}
|
|
181
|
+
disabled={!!readonly}
|
|
182
|
+
classNames='is-full'
|
|
183
|
+
/>
|
|
185
184
|
</div>
|
|
186
185
|
)}
|
|
187
186
|
</List.Root>
|
|
@@ -16,7 +16,7 @@ export const TupleInput = ({
|
|
|
16
16
|
binding,
|
|
17
17
|
type,
|
|
18
18
|
label,
|
|
19
|
-
|
|
19
|
+
readonly,
|
|
20
20
|
getStatus,
|
|
21
21
|
getValue,
|
|
22
22
|
onValueChange,
|
|
@@ -35,7 +35,7 @@ export const TupleInput = ({
|
|
|
35
35
|
<Input.TextInput
|
|
36
36
|
key={prop}
|
|
37
37
|
type='number'
|
|
38
|
-
disabled={
|
|
38
|
+
disabled={!!readonly}
|
|
39
39
|
value={values[prop]}
|
|
40
40
|
onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
|
|
41
41
|
onBlur={onBlur}
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { FormatEnum } from '@dxos/echo-schema';
|
|
6
6
|
import { type SimpleType } from '@dxos/effect';
|
|
7
7
|
|
|
8
|
+
import { GeoPointInput } from './custom';
|
|
8
9
|
import { BooleanInput, MarkdownInput, NumberInput, TextInput } from './Defaults';
|
|
9
10
|
import { type InputComponent } from './Input';
|
|
10
|
-
import { GeoPointInput } from './custom';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Get property input component.
|
|
@@ -6,20 +6,22 @@ import '@dxos-theme';
|
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
8
|
import { Schema } from 'effect';
|
|
9
|
-
import React, { useCallback, useMemo, useState
|
|
9
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
10
10
|
|
|
11
|
-
import { Obj, Type } from '@dxos/echo';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { Filter, Obj, Query, Type } from '@dxos/echo';
|
|
12
|
+
import { type EchoSchema, Format, TypedObject, toJsonSchema } from '@dxos/echo-schema';
|
|
13
|
+
import { useSpace } from '@dxos/react-client/echo';
|
|
14
14
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
15
15
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { QueryParser, createFilter } from '@dxos/react-ui-components';
|
|
17
|
+
import { type DataType, ProjectionModel, createView, typenameFromQuery } from '@dxos/schema';
|
|
18
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
18
19
|
|
|
19
|
-
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
20
20
|
import { translations } from '../../translations';
|
|
21
21
|
import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
22
22
|
|
|
23
|
+
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
24
|
+
|
|
23
25
|
// Type definition for debug objects exposed to tests.
|
|
24
26
|
export type ViewEditorDebugObjects = {
|
|
25
27
|
schema: EchoSchema;
|
|
@@ -27,7 +29,7 @@ export type ViewEditorDebugObjects = {
|
|
|
27
29
|
projection: ProjectionModel;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
type StoryProps = Pick<ViewEditorProps, 'readonly'>;
|
|
32
|
+
type StoryProps = Pick<ViewEditorProps, 'readonly' | 'kind'>;
|
|
31
33
|
|
|
32
34
|
const DefaultStory = (props: StoryProps) => {
|
|
33
35
|
const space = useSpace();
|
|
@@ -57,20 +59,32 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
57
59
|
}
|
|
58
60
|
}, [space]);
|
|
59
61
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(newTypename: string) => {
|
|
64
|
-
if (!schema) {
|
|
62
|
+
const updateViewQuery = useCallback(
|
|
63
|
+
(newQueryString: string) => {
|
|
64
|
+
if (!schema || !view) {
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
|
|
68
|
+
if (props.kind === 'advanced') {
|
|
69
|
+
try {
|
|
70
|
+
const parser = new QueryParser(newQueryString);
|
|
71
|
+
// TODO(wittjosiah): When this fails it should show validation errors in the UI.
|
|
72
|
+
const newQuery = Query.select(createFilter(parser.parse()));
|
|
73
|
+
view.query = newQuery.ast;
|
|
74
|
+
|
|
75
|
+
const typename = typenameFromQuery(newQuery.ast);
|
|
76
|
+
if (typename) {
|
|
77
|
+
schema.updateTypename(typename);
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
} else {
|
|
81
|
+
const typename = newQueryString;
|
|
82
|
+
const newQuery = Query.select(Filter.typename(typename));
|
|
83
|
+
view.query = newQuery.ast;
|
|
84
|
+
schema.updateTypename(typename);
|
|
70
85
|
}
|
|
71
|
-
schema.updateTypename(newTypename);
|
|
72
86
|
},
|
|
73
|
-
[
|
|
87
|
+
[view, schema],
|
|
74
88
|
);
|
|
75
89
|
|
|
76
90
|
const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
|
|
@@ -99,8 +113,9 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
99
113
|
schema={schema}
|
|
100
114
|
view={view}
|
|
101
115
|
registry={space?.db.schemaRegistry}
|
|
116
|
+
kind={props.kind}
|
|
102
117
|
readonly={props.readonly}
|
|
103
|
-
|
|
118
|
+
onQueryChanged={updateViewQuery}
|
|
104
119
|
onDelete={handleDelete}
|
|
105
120
|
/>
|
|
106
121
|
</TestPanel>
|
|
@@ -108,19 +123,31 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
108
123
|
);
|
|
109
124
|
};
|
|
110
125
|
|
|
111
|
-
const meta
|
|
126
|
+
const meta = {
|
|
112
127
|
title: 'ui/react-ui-form/ViewEditor',
|
|
113
128
|
render: DefaultStory,
|
|
114
|
-
decorators: [
|
|
129
|
+
decorators: [
|
|
130
|
+
withClientProvider({
|
|
131
|
+
createSpace: true,
|
|
132
|
+
}),
|
|
133
|
+
withLayout({
|
|
134
|
+
fullscreen: true,
|
|
135
|
+
}),
|
|
136
|
+
withTheme,
|
|
137
|
+
],
|
|
115
138
|
parameters: { translations },
|
|
116
|
-
}
|
|
139
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
117
140
|
|
|
118
141
|
export default meta;
|
|
119
142
|
|
|
120
|
-
type Story = StoryObj<
|
|
143
|
+
type Story = StoryObj<typeof meta>;
|
|
121
144
|
|
|
122
145
|
export const Default: Story = {};
|
|
123
146
|
|
|
124
147
|
export const Readonly: Story = {
|
|
125
148
|
args: { readonly: true },
|
|
126
149
|
};
|
|
150
|
+
|
|
151
|
+
export const Advanced: Story = {
|
|
152
|
+
args: { kind: 'advanced' },
|
|
153
|
+
};
|