@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.a4bbb77
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 +710 -553
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +710 -553
- 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 +81 -5
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +4 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +78 -7
- 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 +128 -31
- 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 +2 -2
- 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 +13 -5
- 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 +76 -4
- 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 +16 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +80 -8
- 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/hooks/useQueryRefOptions.d.ts +7 -2
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +11 -5
- 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 +42 -36
- package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +25 -23
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +21 -23
- package/src/components/Form/ArrayField.tsx +9 -9
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +133 -97
- package/src/components/Form/Form.tsx +16 -12
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +116 -36
- package/src/components/Form/FormContext.tsx +6 -6
- package/src/components/Form/Input.tsx +4 -1
- package/src/components/Form/RefField.tsx +190 -40
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +10 -10
- 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 +110 -46
- package/src/components/ViewEditor/ViewEditor.test.tsx +17 -18
- package/src/components/ViewEditor/ViewEditor.tsx +329 -235
- package/src/components/index.ts +0 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +6 -3
- package/src/hooks/useQueryRefOptions.ts +8 -2
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/translations.ts +13 -6
- 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
|
@@ -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
|
);
|
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import { Schema } from 'effect';
|
|
9
7
|
import React, { useState } from 'react';
|
|
10
8
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { type SelectOption, SelectOptionSchema } from '@dxos/echo-schema';
|
|
10
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
13
11
|
|
|
14
|
-
import { SelectOptionInput } from './SelectOptionsInput';
|
|
15
12
|
import { translations } from '../../../translations';
|
|
16
13
|
import { TestLayout, TestPanel } from '../../testing';
|
|
17
14
|
import { Form } from '../Form';
|
|
18
15
|
|
|
16
|
+
import { SelectOptionInput } from './SelectOptionsInput';
|
|
17
|
+
|
|
19
18
|
const schema = Schema.Struct({
|
|
20
19
|
options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
|
|
21
20
|
}).pipe(Schema.mutable);
|
|
@@ -44,18 +43,19 @@ const DefaultStory = () => {
|
|
|
44
43
|
);
|
|
45
44
|
};
|
|
46
45
|
|
|
47
|
-
const meta
|
|
46
|
+
const meta = {
|
|
48
47
|
title: 'ui/react-ui-form/SelectOptionsInput',
|
|
49
|
-
component: SelectOptionInput,
|
|
48
|
+
component: SelectOptionInput as any,
|
|
50
49
|
render: DefaultStory,
|
|
51
|
-
decorators: [withTheme
|
|
50
|
+
decorators: [withTheme],
|
|
52
51
|
parameters: {
|
|
52
|
+
layout: 'fullscreen',
|
|
53
53
|
translations,
|
|
54
54
|
},
|
|
55
|
-
}
|
|
55
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
56
56
|
|
|
57
57
|
export default meta;
|
|
58
58
|
|
|
59
|
-
type Story = StoryObj
|
|
59
|
+
type Story = StoryObj<typeof meta>;
|
|
60
60
|
|
|
61
61
|
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.
|
|
@@ -2,87 +2,138 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@dxos-theme';
|
|
6
|
-
|
|
7
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import { Schema } from 'effect';
|
|
9
|
-
import React, { useCallback, useMemo,
|
|
7
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
8
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { DXN, Filter, Obj, Query, Type } from '@dxos/echo';
|
|
10
|
+
import { QueryBuilder } from '@dxos/echo-query';
|
|
11
|
+
import { type EchoSchema, Format, toJsonSchema } from '@dxos/echo-schema';
|
|
12
|
+
import { useSpace } from '@dxos/react-client/echo';
|
|
13
13
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
14
14
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
16
|
+
import { type DataType, type ProjectionModel, createView, typenameFromQuery } from '@dxos/schema';
|
|
17
17
|
|
|
18
|
-
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
19
18
|
import { translations } from '../../translations';
|
|
20
19
|
import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
21
20
|
|
|
21
|
+
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
22
|
+
|
|
22
23
|
// Type definition for debug objects exposed to tests.
|
|
23
24
|
export type ViewEditorDebugObjects = {
|
|
24
25
|
schema: EchoSchema;
|
|
25
|
-
view:
|
|
26
|
-
projection:
|
|
26
|
+
view: DataType.View;
|
|
27
|
+
projection: ProjectionModel;
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
type StoryProps = Pick<ViewEditorProps, 'readonly'>;
|
|
30
|
+
type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
|
|
30
31
|
|
|
31
32
|
const DefaultStory = (props: StoryProps) => {
|
|
32
33
|
const space = useSpace();
|
|
33
34
|
const [schema, setSchema] = useState<EchoSchema>();
|
|
34
|
-
const [view, setView] = useState<
|
|
35
|
-
const
|
|
35
|
+
const [view, setView] = useState<DataType.View>();
|
|
36
|
+
const projectionRef = useRef<ProjectionModel>(null);
|
|
37
|
+
|
|
36
38
|
useAsyncEffect(async () => {
|
|
37
39
|
if (space) {
|
|
38
|
-
|
|
40
|
+
const TestSchema = Schema.Struct({
|
|
39
41
|
name: Schema.String,
|
|
40
42
|
email: Format.Email,
|
|
41
43
|
salary: Format.Currency(),
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
}).pipe(
|
|
45
|
+
Type.Obj({
|
|
46
|
+
typename: 'example.com/type/Test',
|
|
47
|
+
version: '0.1.0',
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const AlternateSchema = Schema.Struct({
|
|
52
|
+
title: Schema.String,
|
|
53
|
+
description: Schema.String,
|
|
54
|
+
completed: Schema.Boolean,
|
|
55
|
+
}).pipe(
|
|
56
|
+
Type.Obj({
|
|
57
|
+
typename: 'example.com/type/Alternate',
|
|
58
|
+
version: '0.1.0',
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
|
|
63
|
+
const view = createView({
|
|
64
|
+
name: 'Test',
|
|
65
|
+
query: Query.select(Filter.type(TestSchema)),
|
|
66
|
+
queryString: 'Query.select(Filter.type(TestSchema))',
|
|
67
|
+
jsonSchema: toJsonSchema(TestSchema),
|
|
68
|
+
presentation: Obj.make(Type.Expando, {}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
setSchema(testSchema);
|
|
49
72
|
setView(view);
|
|
50
|
-
setProjection(projection);
|
|
51
73
|
}
|
|
52
74
|
}, [space]);
|
|
53
75
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(newTypename: string) => {
|
|
58
|
-
if (!schema) {
|
|
76
|
+
const updateViewQuery = useCallback(
|
|
77
|
+
async (queryString: string, target?: string) => {
|
|
78
|
+
if (!schema || !view || !space) {
|
|
59
79
|
return;
|
|
60
80
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
view.query.
|
|
81
|
+
|
|
82
|
+
if (props.mode === 'query') {
|
|
83
|
+
view.query.string = queryString;
|
|
84
|
+
|
|
85
|
+
const builder = new QueryBuilder();
|
|
86
|
+
const filter = builder.build(queryString) ?? Filter.nothing();
|
|
87
|
+
const queue = target && DXN.tryParse(target) ? target : undefined;
|
|
88
|
+
const newQuery = queue ? Query.select(filter).options({ queues: [queue] }) : Query.select(filter);
|
|
89
|
+
view.query.ast = newQuery.ast;
|
|
90
|
+
|
|
91
|
+
const typename = typenameFromQuery(newQuery.ast);
|
|
92
|
+
const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
|
|
93
|
+
if (!newSchema) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const newView = createView({
|
|
98
|
+
query: newQuery,
|
|
99
|
+
jsonSchema: newSchema.jsonSchema,
|
|
100
|
+
presentation: Obj.make(Type.Expando, {}),
|
|
101
|
+
});
|
|
102
|
+
view.projection = Obj.getSnapshot(newView).projection;
|
|
103
|
+
setSchema(() => newSchema);
|
|
104
|
+
} else {
|
|
105
|
+
const typename = queryString;
|
|
106
|
+
const newQuery = Query.select(Filter.typename(typename));
|
|
107
|
+
view.query.ast = newQuery.ast;
|
|
108
|
+
schema.updateTypename(typename);
|
|
64
109
|
}
|
|
65
|
-
schema.updateTypename(newTypename);
|
|
66
110
|
},
|
|
67
|
-
[
|
|
111
|
+
[view, schema],
|
|
68
112
|
);
|
|
69
113
|
|
|
70
|
-
const handleDelete = useCallback(
|
|
114
|
+
const handleDelete = useCallback(
|
|
115
|
+
(property: string) => projectionRef.current?.deleteFieldProjection(property),
|
|
116
|
+
[projectionRef],
|
|
117
|
+
);
|
|
71
118
|
|
|
72
119
|
// Expose objects on window for test access.
|
|
73
120
|
useEffect(() => {
|
|
74
|
-
if (typeof window !== 'undefined' && schema && view &&
|
|
75
|
-
(window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
|
|
121
|
+
if (typeof window !== 'undefined' && schema && view && projectionRef.current) {
|
|
122
|
+
(window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
|
|
123
|
+
schema,
|
|
124
|
+
view,
|
|
125
|
+
projection: projectionRef.current,
|
|
126
|
+
} satisfies ViewEditorDebugObjects;
|
|
76
127
|
}
|
|
77
|
-
}, [schema, view
|
|
128
|
+
}, [schema, view]);
|
|
78
129
|
|
|
79
130
|
// NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
|
|
80
131
|
const json = useMemo(
|
|
81
|
-
() => JSON.parse(JSON.stringify({ schema, view, projection })),
|
|
82
|
-
[JSON.stringify(schema), JSON.stringify(view)
|
|
132
|
+
() => JSON.parse(JSON.stringify({ schema, view, projection: projectionRef.current })),
|
|
133
|
+
[JSON.stringify(schema), JSON.stringify(view)],
|
|
83
134
|
);
|
|
84
135
|
|
|
85
|
-
if (!schema || !view
|
|
136
|
+
if (!schema || !view) {
|
|
86
137
|
return <div />;
|
|
87
138
|
}
|
|
88
139
|
|
|
@@ -90,11 +141,13 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
90
141
|
<TestLayout json={json}>
|
|
91
142
|
<TestPanel>
|
|
92
143
|
<ViewEditor
|
|
144
|
+
ref={projectionRef}
|
|
93
145
|
schema={schema}
|
|
94
146
|
view={view}
|
|
95
147
|
registry={space?.db.schemaRegistry}
|
|
148
|
+
mode={props.mode}
|
|
96
149
|
readonly={props.readonly}
|
|
97
|
-
|
|
150
|
+
onQueryChanged={updateViewQuery}
|
|
98
151
|
onDelete={handleDelete}
|
|
99
152
|
/>
|
|
100
153
|
</TestPanel>
|
|
@@ -102,19 +155,30 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
102
155
|
);
|
|
103
156
|
};
|
|
104
157
|
|
|
105
|
-
const meta
|
|
158
|
+
const meta = {
|
|
106
159
|
title: 'ui/react-ui-form/ViewEditor',
|
|
107
160
|
render: DefaultStory,
|
|
108
|
-
decorators: [withClientProvider({ createSpace: true }),
|
|
109
|
-
parameters: {
|
|
110
|
-
|
|
161
|
+
decorators: [withClientProvider({ createSpace: true }), withTheme],
|
|
162
|
+
parameters: {
|
|
163
|
+
layout: 'fullscreen',
|
|
164
|
+
translations,
|
|
165
|
+
},
|
|
166
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
111
167
|
|
|
112
168
|
export default meta;
|
|
113
169
|
|
|
114
|
-
type Story = StoryObj<
|
|
170
|
+
type Story = StoryObj<typeof meta>;
|
|
115
171
|
|
|
116
172
|
export const Default: Story = {};
|
|
117
173
|
|
|
118
174
|
export const Readonly: Story = {
|
|
119
|
-
args: {
|
|
175
|
+
args: {
|
|
176
|
+
readonly: true,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const QueryMode: Story = {
|
|
181
|
+
args: {
|
|
182
|
+
mode: 'query',
|
|
183
|
+
},
|
|
120
184
|
};
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { composeStories } from '@storybook/react';
|
|
6
|
-
import {
|
|
6
|
+
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
7
7
|
import { afterEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
|
-
import { EchoSchema } from '@dxos/echo-schema';
|
|
10
|
-
import {
|
|
9
|
+
import { EchoSchema, isInstanceOf } from '@dxos/echo-schema';
|
|
10
|
+
import { DataType, ProjectionModel } from '@dxos/schema';
|
|
11
|
+
|
|
12
|
+
import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
11
13
|
|
|
12
14
|
import * as stories from './ViewEditor.stories';
|
|
13
15
|
import { type ViewEditorDebugObjects } from './ViewEditor.stories';
|
|
14
|
-
import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
15
16
|
|
|
16
17
|
const { Default } = composeStories(stories);
|
|
17
18
|
|
|
@@ -19,8 +20,8 @@ const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
|
|
|
19
20
|
const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
|
|
20
21
|
expect(debugObjects).toBeDefined();
|
|
21
22
|
expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
|
|
22
|
-
expect(debugObjects.view).
|
|
23
|
-
expect(debugObjects.projection).toBeInstanceOf(
|
|
23
|
+
expect(isInstanceOf(DataType.View, debugObjects.view)).toBeTruthy();
|
|
24
|
+
expect(debugObjects.projection).toBeInstanceOf(ProjectionModel);
|
|
24
25
|
return debugObjects;
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -41,7 +42,7 @@ describe('ViewEditor', () => {
|
|
|
41
42
|
test('renders view editor', async () => {
|
|
42
43
|
await Default.run();
|
|
43
44
|
await waitForViewEditor();
|
|
44
|
-
expect(screen.getByText('
|
|
45
|
+
expect(screen.getByText('Record type')).toBeInTheDocument();
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
test('change property name', async () => {
|
|
@@ -73,7 +74,7 @@ describe('ViewEditor', () => {
|
|
|
73
74
|
expect(schemaProperties!.name).toBeUndefined();
|
|
74
75
|
|
|
75
76
|
// Check view contains new_property field.
|
|
76
|
-
const newPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'new_property');
|
|
77
|
+
const newPropertyField = debugObjects.view.projection.fields.find((field: any) => field.path === 'new_property');
|
|
77
78
|
expect(newPropertyField).toBeDefined();
|
|
78
79
|
expect(newPropertyField!.path).toBe('new_property');
|
|
79
80
|
|
|
@@ -120,7 +121,9 @@ describe('ViewEditor', () => {
|
|
|
120
121
|
expect(schemaProperties!.added_property).toBeDefined();
|
|
121
122
|
|
|
122
123
|
// Check view contains added_property field.
|
|
123
|
-
const addedPropertyField = debugObjects.view.fields.find(
|
|
124
|
+
const addedPropertyField = debugObjects.view.projection.fields.find(
|
|
125
|
+
(field: any) => field.path === 'added_property',
|
|
126
|
+
);
|
|
124
127
|
expect(addedPropertyField).toBeDefined();
|
|
125
128
|
expect(addedPropertyField!.path).toBe('added_property');
|
|
126
129
|
|
|
@@ -138,7 +141,7 @@ describe('ViewEditor', () => {
|
|
|
138
141
|
// Find the delete button for the 'name' property.
|
|
139
142
|
const nameProperty = screen.getByText('name');
|
|
140
143
|
const propertyRow = nameProperty.closest('[role="listitem"]');
|
|
141
|
-
const deleteButton = propertyRow?.querySelector('
|
|
144
|
+
const deleteButton = propertyRow?.querySelector('[data-testid="field.delete"]');
|
|
142
145
|
|
|
143
146
|
if (!deleteButton) {
|
|
144
147
|
throw new Error('Delete button not found');
|
|
@@ -161,7 +164,7 @@ describe('ViewEditor', () => {
|
|
|
161
164
|
expect(schemaProperties!.name).toBeUndefined();
|
|
162
165
|
|
|
163
166
|
// Check view no longer contains the name field.
|
|
164
|
-
const nameField = debugObjects.view.fields.find((field: any) => field.path === 'name');
|
|
167
|
+
const nameField = debugObjects.view.projection.fields.find((field: any) => field.path === 'name');
|
|
165
168
|
expect(nameField).toBeUndefined();
|
|
166
169
|
|
|
167
170
|
// Check projection no longer contains the name property.
|
|
@@ -186,19 +189,15 @@ describe('ViewEditor', () => {
|
|
|
186
189
|
// Access the live objects directly from window using symbol.
|
|
187
190
|
const debugObjects = getViewEditorDebugObjects();
|
|
188
191
|
|
|
189
|
-
// Check that hiddenFields is non-empty.
|
|
190
|
-
expect(debugObjects.view.hiddenFields).toBeDefined();
|
|
191
|
-
expect(debugObjects.view.hiddenFields!.length).toBeGreaterThan(0);
|
|
192
|
-
|
|
193
192
|
// Click the show button
|
|
194
193
|
fireEvent.click(screen.getByTestId('show-field-button'));
|
|
195
194
|
|
|
196
195
|
// Wait for the data to update and check that hiddenFields is empty.
|
|
197
196
|
await waitFor(() => {
|
|
198
|
-
expect(debugObjects.view.
|
|
197
|
+
expect(debugObjects.view.projection.fields.filter(({ visible }) => visible === false)!.length).toBe(0);
|
|
199
198
|
});
|
|
200
199
|
|
|
201
200
|
// Also verify that the field is back in the visible fields list.
|
|
202
|
-
expect(debugObjects.view.fields.length).toBeGreaterThan(0);
|
|
201
|
+
expect(debugObjects.view.projection.fields.filter(({ visible }) => visible !== false).length).toBeGreaterThan(0);
|
|
203
202
|
});
|
|
204
|
-
});
|
|
203
|
+
}, 30_000);
|