@dxos/react-ui-form 0.8.4-main.f9ba587 → 0.8.4-main.fd6878d
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 +184 -171
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +184 -171
- 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.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +4 -4
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +2 -2
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- 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 +1 -1
- 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/RefField.d.ts +1 -1
- package/dist/types/src/components/Form/RefField.d.ts.map +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.map +1 -1
- package/dist/types/src/components/Form/factory.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 +3 -3
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.d.ts +3 -3
- package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
- package/dist/types/src/components/deprecated/Form.stories.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 +3 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +30 -29
- package/src/components/ControlSection/ControlSection.stories.tsx +2 -1
- package/src/components/FieldEditor/FieldEditor.stories.tsx +16 -13
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +11 -11
- package/src/components/Form/ArrayField.tsx +8 -7
- package/src/components/Form/Defaults.tsx +2 -1
- package/src/components/Form/Form.stories.tsx +4 -3
- package/src/components/Form/Form.tsx +4 -3
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +6 -5
- package/src/components/Form/FormContext.tsx +5 -5
- package/src/components/Form/RefField.tsx +6 -5
- package/src/components/Form/custom/GeoPointInput.tsx +2 -2
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +3 -2
- package/src/components/Form/custom/SelectOptionsInput.tsx +28 -29
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +19 -12
- package/src/components/ViewEditor/ViewEditor.test.tsx +18 -15
- package/src/components/ViewEditor/ViewEditor.tsx +81 -81
- package/src/components/deprecated/Form.stories.tsx +6 -5
- package/src/components/deprecated/Form.tsx +6 -6
- package/src/hooks/useForm.ts +5 -3
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/translations.ts +3 -2
|
@@ -2,19 +2,18 @@
|
|
|
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
|
-
// TODO(ZaymonFC): Handle disabled.
|
|
18
17
|
export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue, onValueChange }: InputProps) => {
|
|
19
18
|
const [selected, setSelectedId] = useState<string | null>(null);
|
|
20
19
|
const [isNewOption, setIsNewOption] = useState(false);
|
|
@@ -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={disabled ? undefined : handleMove}
|
|
121
|
+
readonly={disabled}
|
|
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={disabled} />
|
|
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={disabled}
|
|
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={disabled} value={item.color} onChange={handleColorChange(item.id)} />
|
|
161
163
|
<IconButton
|
|
164
|
+
disabled={disabled}
|
|
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={disabled}
|
|
182
|
+
classNames='is-full'
|
|
183
|
+
/>
|
|
185
184
|
</div>
|
|
186
185
|
)}
|
|
187
186
|
</List.Root>
|
|
@@ -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,24 +6,26 @@ 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 {
|
|
11
|
+
import { Obj, Type } from '@dxos/echo';
|
|
12
|
+
import { type EchoSchema, Format, TypedObject, toJsonSchema } from '@dxos/echo-schema';
|
|
12
13
|
import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
13
14
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
14
15
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
16
|
+
import { DataType, ProjectionModel, createView } from '@dxos/schema';
|
|
17
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
17
18
|
|
|
18
|
-
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
19
19
|
import { translations } from '../../translations';
|
|
20
20
|
import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
21
21
|
|
|
22
|
+
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
23
|
+
|
|
22
24
|
// Type definition for debug objects exposed to tests.
|
|
23
25
|
export type ViewEditorDebugObjects = {
|
|
24
26
|
schema: EchoSchema;
|
|
25
|
-
view:
|
|
26
|
-
projection:
|
|
27
|
+
view: DataType.View;
|
|
28
|
+
projection: ProjectionModel;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
type StoryProps = Pick<ViewEditorProps, 'readonly'>;
|
|
@@ -31,8 +33,8 @@ type StoryProps = Pick<ViewEditorProps, 'readonly'>;
|
|
|
31
33
|
const DefaultStory = (props: StoryProps) => {
|
|
32
34
|
const space = useSpace();
|
|
33
35
|
const [schema, setSchema] = useState<EchoSchema>();
|
|
34
|
-
const [view, setView] = useState<
|
|
35
|
-
const [projection, setProjection] = useState<
|
|
36
|
+
const [view, setView] = useState<DataType.View>();
|
|
37
|
+
const [projection, setProjection] = useState<ProjectionModel>();
|
|
36
38
|
useAsyncEffect(async () => {
|
|
37
39
|
if (space) {
|
|
38
40
|
class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
@@ -42,8 +44,13 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
42
44
|
}) {}
|
|
43
45
|
|
|
44
46
|
const [schema] = await space.db.schemaRegistry.register([TestSchema]);
|
|
45
|
-
const view = createView({
|
|
46
|
-
|
|
47
|
+
const view = createView({
|
|
48
|
+
name: 'Test',
|
|
49
|
+
typename: schema.typename,
|
|
50
|
+
jsonSchema: toJsonSchema(TestSchema),
|
|
51
|
+
presentation: Obj.make(Type.Expando, {}),
|
|
52
|
+
});
|
|
53
|
+
const projection = new ProjectionModel(schema.jsonSchema, view.projection);
|
|
47
54
|
|
|
48
55
|
setSchema(schema);
|
|
49
56
|
setView(view);
|
|
@@ -51,7 +58,7 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
51
58
|
}
|
|
52
59
|
}, [space]);
|
|
53
60
|
|
|
54
|
-
const views = useQuery(space, Filter.type(
|
|
61
|
+
const views = useQuery(space, Filter.type(DataType.View));
|
|
55
62
|
const currentTypename = useMemo(() => view?.query?.typename, [view]);
|
|
56
63
|
const updateViewTypename = useCallback(
|
|
57
64
|
(newTypename: string) => {
|
|
@@ -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.
|
|
@@ -187,18 +190,18 @@ describe('ViewEditor', () => {
|
|
|
187
190
|
const debugObjects = getViewEditorDebugObjects();
|
|
188
191
|
|
|
189
192
|
// Check that hiddenFields is non-empty.
|
|
190
|
-
expect(debugObjects.view.hiddenFields).toBeDefined();
|
|
191
|
-
expect(debugObjects.view.hiddenFields!.length).toBeGreaterThan(0);
|
|
193
|
+
expect(debugObjects.view.projection.hiddenFields).toBeDefined();
|
|
194
|
+
expect(debugObjects.view.projection.hiddenFields!.length).toBeGreaterThan(0);
|
|
192
195
|
|
|
193
196
|
// Click the show button
|
|
194
197
|
fireEvent.click(screen.getByTestId('show-field-button'));
|
|
195
198
|
|
|
196
199
|
// Wait for the data to update and check that hiddenFields is empty.
|
|
197
200
|
await waitFor(() => {
|
|
198
|
-
expect(debugObjects.view.hiddenFields!.length).toBe(0);
|
|
201
|
+
expect(debugObjects.view.projection.hiddenFields!.length).toBe(0);
|
|
199
202
|
});
|
|
200
203
|
|
|
201
204
|
// Also verify that the field is back in the visible fields list.
|
|
202
|
-
expect(debugObjects.view.fields.length).toBeGreaterThan(0);
|
|
205
|
+
expect(debugObjects.view.projection.fields.length).toBeGreaterThan(0);
|
|
203
206
|
});
|
|
204
207
|
});
|
|
@@ -11,21 +11,19 @@ import { invariant } from '@dxos/invariant';
|
|
|
11
11
|
import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
12
12
|
import { List } from '@dxos/react-ui-list';
|
|
13
13
|
import { cardSpacing } from '@dxos/react-ui-stack';
|
|
14
|
-
import {
|
|
15
|
-
import { FieldSchema, type FieldType,
|
|
14
|
+
import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
|
|
15
|
+
import { type DataType, FieldSchema, type FieldType, ProjectionModel, VIEW_FIELD_LIMIT } from '@dxos/schema';
|
|
16
16
|
|
|
17
17
|
import { translationKey } from '../../translations';
|
|
18
18
|
import { FieldEditor } from '../FieldEditor';
|
|
19
19
|
import { Form, type FormProps } from '../Form';
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
22
|
+
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
22
23
|
|
|
23
24
|
const ViewMetaSchema = Schema.Struct({
|
|
24
|
-
name: Schema.String.annotations({
|
|
25
|
-
title: 'View',
|
|
26
|
-
}),
|
|
27
25
|
typename: Format.URL.annotations({
|
|
28
|
-
title: '
|
|
26
|
+
title: 'Record type',
|
|
29
27
|
}),
|
|
30
28
|
}).pipe(Schema.mutable);
|
|
31
29
|
|
|
@@ -34,7 +32,7 @@ type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
|
|
|
34
32
|
export type ViewEditorProps = ThemedClassName<
|
|
35
33
|
{
|
|
36
34
|
schema: Schema.Schema.AnyNoContext;
|
|
37
|
-
view:
|
|
35
|
+
view: DataType.View;
|
|
38
36
|
registry?: SchemaRegistry;
|
|
39
37
|
readonly?: boolean;
|
|
40
38
|
showHeading?: boolean;
|
|
@@ -61,27 +59,23 @@ export const ViewEditor = ({
|
|
|
61
59
|
const projection = useMemo(() => {
|
|
62
60
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
63
61
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
64
|
-
return new
|
|
65
|
-
}, [schema, view]);
|
|
66
|
-
const [
|
|
62
|
+
return new ProjectionModel(jsonSchema, view.projection);
|
|
63
|
+
}, [schema, view.projection]);
|
|
64
|
+
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
67
65
|
const readonly = _readonly || !isMutable(schema);
|
|
68
66
|
|
|
69
67
|
// TODO(burdon): Should be reactive.
|
|
70
68
|
const viewValues = useMemo(() => {
|
|
71
69
|
return {
|
|
72
|
-
name: view.name,
|
|
73
70
|
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
74
71
|
// TODO(burdon): Settings should have domain name owned by user.
|
|
75
72
|
typename: view.query.typename,
|
|
76
73
|
};
|
|
77
|
-
}, [view]);
|
|
74
|
+
}, [view.query.typename]);
|
|
78
75
|
|
|
79
|
-
const
|
|
76
|
+
const handleToggleField = useCallback(
|
|
80
77
|
(field: FieldType) => {
|
|
81
|
-
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
setField((f) => (f === field ? undefined : field));
|
|
78
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
85
79
|
},
|
|
86
80
|
[readonly],
|
|
87
81
|
);
|
|
@@ -91,56 +85,52 @@ export const ViewEditor = ({
|
|
|
91
85
|
const handleAdd = useCallback(() => {
|
|
92
86
|
invariant(!readonly);
|
|
93
87
|
const field = projection.createFieldProjection();
|
|
94
|
-
|
|
95
|
-
}, [schema,
|
|
88
|
+
setExpandedField(field.id);
|
|
89
|
+
}, [schema, projection, readonly]);
|
|
96
90
|
|
|
97
91
|
const handleUpdate = useCallback(
|
|
98
|
-
({
|
|
92
|
+
({ typename }: ViewMetaType) => {
|
|
99
93
|
invariant(!readonly);
|
|
100
94
|
requestAnimationFrame(() => {
|
|
101
|
-
if (view.name !== name) {
|
|
102
|
-
view.name = name;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
95
|
if (view.query.typename !== typename && !readonly) {
|
|
106
96
|
onTypenameChanged?.(typename);
|
|
107
97
|
}
|
|
108
98
|
});
|
|
109
99
|
},
|
|
110
|
-
[
|
|
100
|
+
[view.query.typename, onTypenameChanged, readonly],
|
|
111
101
|
);
|
|
112
102
|
|
|
113
103
|
const handleDelete = useCallback(
|
|
114
104
|
(fieldId: string) => {
|
|
115
105
|
invariant(!readonly);
|
|
116
|
-
if (fieldId ===
|
|
117
|
-
|
|
106
|
+
if (fieldId === expandedField) {
|
|
107
|
+
setExpandedField(undefined);
|
|
118
108
|
}
|
|
119
109
|
|
|
120
110
|
onDelete?.(fieldId);
|
|
121
111
|
},
|
|
122
|
-
[
|
|
112
|
+
[expandedField, onDelete, readonly],
|
|
123
113
|
);
|
|
124
114
|
|
|
125
115
|
const handleMove = useCallback(
|
|
126
116
|
(fromIndex: number, toIndex: number) => {
|
|
127
117
|
invariant(!readonly);
|
|
128
118
|
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
129
|
-
const fields = [...view.fields];
|
|
119
|
+
const fields = [...view.projection.fields];
|
|
130
120
|
const [moved] = fields.splice(fromIndex, 1);
|
|
131
121
|
fields.splice(toIndex, 0, moved);
|
|
132
|
-
view.fields = fields;
|
|
122
|
+
view.projection.fields = fields;
|
|
133
123
|
},
|
|
134
|
-
[
|
|
124
|
+
[view.projection.fields, readonly],
|
|
135
125
|
);
|
|
136
126
|
|
|
137
|
-
const handleClose = useCallback(() =>
|
|
127
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
138
128
|
|
|
139
129
|
const hiddenProperties = projection.getHiddenProperties();
|
|
140
130
|
|
|
141
131
|
const handleHide = useCallback(
|
|
142
132
|
(fieldId: string) => {
|
|
143
|
-
|
|
133
|
+
setExpandedField(undefined);
|
|
144
134
|
projection.hideFieldProjection(fieldId);
|
|
145
135
|
},
|
|
146
136
|
[projection],
|
|
@@ -148,7 +138,7 @@ export const ViewEditor = ({
|
|
|
148
138
|
|
|
149
139
|
const handleShow = useCallback(
|
|
150
140
|
(property: string) => {
|
|
151
|
-
|
|
141
|
+
setExpandedField(undefined);
|
|
152
142
|
projection.showFieldProjection(property as JsonProp);
|
|
153
143
|
},
|
|
154
144
|
[projection],
|
|
@@ -157,11 +147,12 @@ export const ViewEditor = ({
|
|
|
157
147
|
return (
|
|
158
148
|
<div role='none' className={mx(classNames)}>
|
|
159
149
|
{readonly && (
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
150
|
+
<Callout.Root
|
|
151
|
+
valence='info'
|
|
152
|
+
classNames={['is-full mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}
|
|
153
|
+
>
|
|
154
|
+
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
155
|
+
</Callout.Root>
|
|
165
156
|
)}
|
|
166
157
|
|
|
167
158
|
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
@@ -176,46 +167,65 @@ export const ViewEditor = ({
|
|
|
176
167
|
/>
|
|
177
168
|
|
|
178
169
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
179
|
-
<
|
|
170
|
+
<h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
|
|
180
171
|
|
|
181
172
|
<List.Root<FieldType>
|
|
182
|
-
items={view.fields}
|
|
173
|
+
items={view.projection.fields}
|
|
183
174
|
isItem={Schema.is(FieldSchema)}
|
|
184
175
|
getId={(field) => field.id}
|
|
185
|
-
readonly={readonly}
|
|
186
176
|
onMove={readonly ? undefined : handleMove}
|
|
177
|
+
readonly={readonly}
|
|
187
178
|
>
|
|
188
179
|
{({ items: fields }) => (
|
|
189
180
|
<>
|
|
190
|
-
{showHeading && (
|
|
191
|
-
<div role='heading' className={grid}>
|
|
192
|
-
<div />
|
|
193
|
-
<div className='flex items-center text-sm'>{t('field path label')}</div>
|
|
194
|
-
</div>
|
|
195
|
-
)}
|
|
181
|
+
{showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
|
|
196
182
|
|
|
197
|
-
<div role='list' className=
|
|
183
|
+
<div role='list' className={listGrid}>
|
|
198
184
|
{fields?.map((field) => (
|
|
199
185
|
<List.Item<FieldType>
|
|
200
186
|
key={field.id}
|
|
201
187
|
item={field}
|
|
202
|
-
classNames={
|
|
188
|
+
classNames={listItemGrid}
|
|
189
|
+
aria-expanded={expandedField === field.id}
|
|
203
190
|
>
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
191
|
+
<div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
|
|
192
|
+
<List.ItemDragHandle disabled={readonly} />
|
|
193
|
+
<List.ItemTitle onClick={() => handleToggleField(field)}>{field.path}</List.ItemTitle>
|
|
194
|
+
<List.ItemButton
|
|
195
|
+
label={t('hide field label')}
|
|
196
|
+
data-testid='hide-field-button'
|
|
197
|
+
icon='ph--eye-slash--regular'
|
|
198
|
+
autoHide={false}
|
|
199
|
+
// TDOO(burdon): Is this the correct test?
|
|
200
|
+
disabled={readonly || view.projection.fields.length <= 1}
|
|
201
|
+
onClick={() => handleHide(field.id)}
|
|
202
|
+
/>
|
|
203
|
+
{/* TODO(burdon): Remove unless implement undo. */}
|
|
215
204
|
<List.ItemDeleteButton
|
|
216
|
-
|
|
205
|
+
label={t('delete field label')}
|
|
206
|
+
autoHide={false}
|
|
207
|
+
disabled={readonly || view.projection.fields.length <= 1}
|
|
217
208
|
onClick={() => handleDelete(field.id)}
|
|
209
|
+
data-testid='field.delete'
|
|
218
210
|
/>
|
|
211
|
+
<IconButton
|
|
212
|
+
iconOnly
|
|
213
|
+
variant='ghost'
|
|
214
|
+
label={t('toggle expand label', { ns: 'os' })}
|
|
215
|
+
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
216
|
+
onClick={() => handleToggleField(field)}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
{expandedField === field.id && (
|
|
220
|
+
<div role='none' className='col-span-5'>
|
|
221
|
+
<FieldEditor
|
|
222
|
+
readonly={readonly}
|
|
223
|
+
projection={projection}
|
|
224
|
+
field={field}
|
|
225
|
+
registry={registry}
|
|
226
|
+
onSave={handleClose}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
219
229
|
)}
|
|
220
230
|
</List.Item>
|
|
221
231
|
))}
|
|
@@ -226,7 +236,7 @@ export const ViewEditor = ({
|
|
|
226
236
|
|
|
227
237
|
{hiddenProperties.length > 0 && (
|
|
228
238
|
<>
|
|
229
|
-
<
|
|
239
|
+
<h2 className={mx(inputTextLabel)}>{t('hidden fields label')}</h2>
|
|
230
240
|
|
|
231
241
|
<List.Root<string>
|
|
232
242
|
items={hiddenProperties}
|
|
@@ -234,16 +244,17 @@ export const ViewEditor = ({
|
|
|
234
244
|
getId={(property) => property}
|
|
235
245
|
>
|
|
236
246
|
{({ items: properties }) => (
|
|
237
|
-
<div role='list' className='
|
|
247
|
+
<div role='list' className='grid grid-cols-[1fr_min-content]'>
|
|
238
248
|
{properties?.map((property) => (
|
|
239
249
|
<List.Item<string>
|
|
240
250
|
key={property}
|
|
241
251
|
item={property}
|
|
242
|
-
classNames={mx(grid
|
|
252
|
+
classNames={mx('grid grid-cols-subgrid col-span-2 rounded-sm', subtleHover)}
|
|
243
253
|
>
|
|
244
|
-
<List.ItemDragHandle disabled />
|
|
245
254
|
<List.ItemTitle>{property}</List.ItemTitle>
|
|
246
255
|
<List.ItemButton
|
|
256
|
+
label={t('show field label')}
|
|
257
|
+
disabled={readonly}
|
|
247
258
|
icon='ph--eye--regular'
|
|
248
259
|
onClick={() => handleShow(property)}
|
|
249
260
|
data-testid='show-field-button'
|
|
@@ -257,26 +268,15 @@ export const ViewEditor = ({
|
|
|
257
268
|
)}
|
|
258
269
|
</div>
|
|
259
270
|
|
|
260
|
-
{
|
|
261
|
-
<FieldEditor
|
|
262
|
-
key={field.id}
|
|
263
|
-
view={view}
|
|
264
|
-
projection={projection}
|
|
265
|
-
field={field}
|
|
266
|
-
registry={registry}
|
|
267
|
-
onSave={handleClose}
|
|
268
|
-
/>
|
|
269
|
-
)}
|
|
270
|
-
|
|
271
|
-
{!readonly && !field && (
|
|
271
|
+
{!readonly && !expandedField && (
|
|
272
272
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
273
273
|
<IconButton
|
|
274
274
|
icon='ph--plus--regular'
|
|
275
275
|
label={t('button add property')}
|
|
276
276
|
onClick={readonly ? undefined : handleAdd}
|
|
277
277
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
278
|
-
disabled={view.fields.length >= VIEW_FIELD_LIMIT}
|
|
279
|
-
classNames='
|
|
278
|
+
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
279
|
+
classNames='is-full'
|
|
280
280
|
/>
|
|
281
281
|
</div>
|
|
282
282
|
)}
|
|
@@ -8,13 +8,14 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
8
8
|
import React from 'react';
|
|
9
9
|
|
|
10
10
|
import { live } from '@dxos/live-object';
|
|
11
|
-
import {
|
|
11
|
+
import { TestSchema, type TestType, testData, testView } from '@dxos/schema/testing';
|
|
12
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
13
13
|
|
|
14
|
-
import { DeprecatedForm, type DeprecatedFormProps } from './Form';
|
|
15
14
|
import { translations } from '../../translations';
|
|
16
15
|
import { TestLayout, TestPanel } from '../testing';
|
|
17
16
|
|
|
17
|
+
import { DeprecatedForm, type DeprecatedFormProps } from './Form';
|
|
18
|
+
|
|
18
19
|
const DefaultStory = (props: DeprecatedFormProps) => (
|
|
19
20
|
<TestLayout json={{ props }}>
|
|
20
21
|
<TestPanel>
|
|
@@ -40,14 +41,14 @@ type Story = StoryObj<DeprecatedFormProps<TestType>>;
|
|
|
40
41
|
export const Default: Story = {
|
|
41
42
|
args: {
|
|
42
43
|
object: live(testData),
|
|
43
|
-
|
|
44
|
+
projection: live(testView).projection,
|
|
44
45
|
schema: TestSchema,
|
|
45
46
|
},
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
export const Empty: Story = {
|
|
49
50
|
args: {
|
|
50
|
-
|
|
51
|
+
projection: live(testView).projection,
|
|
51
52
|
schema: TestSchema,
|
|
52
53
|
},
|
|
53
54
|
};
|
|
@@ -55,7 +56,7 @@ export const Empty: Story = {
|
|
|
55
56
|
export const Readonly: Story = {
|
|
56
57
|
args: {
|
|
57
58
|
object: live(testData),
|
|
58
|
-
|
|
59
|
+
projection: live(testView).projection,
|
|
59
60
|
schema: TestSchema,
|
|
60
61
|
readonly: true,
|
|
61
62
|
},
|