@dxos/react-ui-form 0.8.4-main.c1de068 → 0.8.4-main.e098934
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 +58 -26
- 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
|
@@ -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, 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();
|
|
@@ -36,16 +38,21 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
36
38
|
const [projection, setProjection] = useState<ProjectionModel>();
|
|
37
39
|
useAsyncEffect(async () => {
|
|
38
40
|
if (space) {
|
|
39
|
-
|
|
41
|
+
const TestSchema = Schema.Struct({
|
|
40
42
|
name: Schema.String,
|
|
41
43
|
email: Format.Email,
|
|
42
44
|
salary: Format.Currency(),
|
|
43
|
-
})
|
|
45
|
+
}).pipe(
|
|
46
|
+
Type.Obj({
|
|
47
|
+
typename: 'example.com/type/Test',
|
|
48
|
+
version: '0.1.0',
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
44
51
|
|
|
45
52
|
const [schema] = await space.db.schemaRegistry.register([TestSchema]);
|
|
46
53
|
const view = createView({
|
|
47
54
|
name: 'Test',
|
|
48
|
-
|
|
55
|
+
query: Query.select(Filter.type(TestSchema)),
|
|
49
56
|
jsonSchema: toJsonSchema(TestSchema),
|
|
50
57
|
presentation: Obj.make(Type.Expando, {}),
|
|
51
58
|
});
|
|
@@ -57,20 +64,32 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
57
64
|
}
|
|
58
65
|
}, [space]);
|
|
59
66
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(newTypename: string) => {
|
|
64
|
-
if (!schema) {
|
|
67
|
+
const updateViewQuery = useCallback(
|
|
68
|
+
(newQueryString: string) => {
|
|
69
|
+
if (!schema || !view) {
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
|
|
73
|
+
if (props.kind === 'advanced') {
|
|
74
|
+
try {
|
|
75
|
+
const parser = new QueryParser(newQueryString);
|
|
76
|
+
// TODO(wittjosiah): When this fails it should show validation errors in the UI.
|
|
77
|
+
const newQuery = Query.select(createFilter(parser.parse()));
|
|
78
|
+
view.query = newQuery.ast;
|
|
79
|
+
|
|
80
|
+
const typename = typenameFromQuery(newQuery.ast);
|
|
81
|
+
if (typename) {
|
|
82
|
+
schema.updateTypename(typename);
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
} else {
|
|
86
|
+
const typename = newQueryString;
|
|
87
|
+
const newQuery = Query.select(Filter.typename(typename));
|
|
88
|
+
view.query = newQuery.ast;
|
|
89
|
+
schema.updateTypename(typename);
|
|
70
90
|
}
|
|
71
|
-
schema.updateTypename(newTypename);
|
|
72
91
|
},
|
|
73
|
-
[
|
|
92
|
+
[view, schema],
|
|
74
93
|
);
|
|
75
94
|
|
|
76
95
|
const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
|
|
@@ -99,8 +118,9 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
99
118
|
schema={schema}
|
|
100
119
|
view={view}
|
|
101
120
|
registry={space?.db.schemaRegistry}
|
|
121
|
+
kind={props.kind}
|
|
102
122
|
readonly={props.readonly}
|
|
103
|
-
|
|
123
|
+
onQueryChanged={updateViewQuery}
|
|
104
124
|
onDelete={handleDelete}
|
|
105
125
|
/>
|
|
106
126
|
</TestPanel>
|
|
@@ -108,19 +128,31 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
108
128
|
);
|
|
109
129
|
};
|
|
110
130
|
|
|
111
|
-
const meta
|
|
131
|
+
const meta = {
|
|
112
132
|
title: 'ui/react-ui-form/ViewEditor',
|
|
113
133
|
render: DefaultStory,
|
|
114
|
-
decorators: [
|
|
134
|
+
decorators: [
|
|
135
|
+
withClientProvider({
|
|
136
|
+
createSpace: true,
|
|
137
|
+
}),
|
|
138
|
+
withLayout({
|
|
139
|
+
fullscreen: true,
|
|
140
|
+
}),
|
|
141
|
+
withTheme,
|
|
142
|
+
],
|
|
115
143
|
parameters: { translations },
|
|
116
|
-
}
|
|
144
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
117
145
|
|
|
118
146
|
export default meta;
|
|
119
147
|
|
|
120
|
-
type Story = StoryObj<
|
|
148
|
+
type Story = StoryObj<typeof meta>;
|
|
121
149
|
|
|
122
150
|
export const Default: Story = {};
|
|
123
151
|
|
|
124
152
|
export const Readonly: Story = {
|
|
125
153
|
args: { readonly: true },
|
|
126
154
|
};
|
|
155
|
+
|
|
156
|
+
export const Advanced: Story = {
|
|
157
|
+
args: { kind: 'advanced' },
|
|
158
|
+
};
|
|
@@ -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
9
|
import { EchoSchema, isInstanceOf } from '@dxos/echo-schema';
|
|
10
10
|
import { DataType, ProjectionModel } from '@dxos/schema';
|
|
11
11
|
|
|
12
|
+
import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
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
|
|
|
@@ -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 () => {
|
|
@@ -140,7 +141,7 @@ describe('ViewEditor', () => {
|
|
|
140
141
|
// Find the delete button for the 'name' property.
|
|
141
142
|
const nameProperty = screen.getByText('name');
|
|
142
143
|
const propertyRow = nameProperty.closest('[role="listitem"]');
|
|
143
|
-
const deleteButton = propertyRow?.querySelector('
|
|
144
|
+
const deleteButton = propertyRow?.querySelector('[data-testid="field.delete"]');
|
|
144
145
|
|
|
145
146
|
if (!deleteButton) {
|
|
146
147
|
throw new Error('Delete button not found');
|
|
@@ -188,19 +189,15 @@ describe('ViewEditor', () => {
|
|
|
188
189
|
// Access the live objects directly from window using symbol.
|
|
189
190
|
const debugObjects = getViewEditorDebugObjects();
|
|
190
191
|
|
|
191
|
-
// Check that hiddenFields is non-empty.
|
|
192
|
-
expect(debugObjects.view.projection.hiddenFields).toBeDefined();
|
|
193
|
-
expect(debugObjects.view.projection.hiddenFields!.length).toBeGreaterThan(0);
|
|
194
|
-
|
|
195
192
|
// Click the show button
|
|
196
193
|
fireEvent.click(screen.getByTestId('show-field-button'));
|
|
197
194
|
|
|
198
195
|
// Wait for the data to update and check that hiddenFields is empty.
|
|
199
196
|
await waitFor(() => {
|
|
200
|
-
expect(debugObjects.view.projection.
|
|
197
|
+
expect(debugObjects.view.projection.fields.filter(({ visible }) => visible === false)!.length).toBe(0);
|
|
201
198
|
});
|
|
202
199
|
|
|
203
200
|
// Also verify that the field is back in the visible fields list.
|
|
204
|
-
expect(debugObjects.view.projection.fields.length).toBeGreaterThan(0);
|
|
201
|
+
expect(debugObjects.view.projection.fields.filter(({ visible }) => visible !== false).length).toBeGreaterThan(0);
|
|
205
202
|
});
|
|
206
203
|
});
|
|
@@ -2,40 +2,43 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Schema } from 'effect';
|
|
5
|
+
import { Match, Schema } from 'effect';
|
|
6
6
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type SchemaRegistry } from '@dxos/echo-db';
|
|
9
9
|
import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo-schema';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
|
+
import { getSpace } from '@dxos/react-client/echo';
|
|
11
12
|
import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
13
|
+
import { QueryEditor, QuerySerializer, createExpression } from '@dxos/react-ui-components';
|
|
12
14
|
import { List } from '@dxos/react-ui-list';
|
|
13
15
|
import { cardSpacing } from '@dxos/react-ui-stack';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
+
import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
|
|
17
|
+
import {
|
|
18
|
+
type DataType,
|
|
19
|
+
FieldSchema,
|
|
20
|
+
type FieldType,
|
|
21
|
+
ProjectionModel,
|
|
22
|
+
VIEW_FIELD_LIMIT,
|
|
23
|
+
typenameFromQuery,
|
|
24
|
+
} from '@dxos/schema';
|
|
16
25
|
|
|
17
26
|
import { translationKey } from '../../translations';
|
|
18
27
|
import { FieldEditor } from '../FieldEditor';
|
|
19
|
-
import { Form, type FormProps } from '../Form';
|
|
28
|
+
import { Form, type FormProps, type InputComponent, type InputProps } from '../Form';
|
|
20
29
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const ViewMetaSchema = Schema.Struct({
|
|
24
|
-
typename: Format.URL.annotations({
|
|
25
|
-
title: 'Typename',
|
|
26
|
-
}),
|
|
27
|
-
}).pipe(Schema.mutable);
|
|
28
|
-
|
|
29
|
-
type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
|
|
30
|
+
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
31
|
+
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
30
32
|
|
|
31
33
|
export type ViewEditorProps = ThemedClassName<
|
|
32
34
|
{
|
|
33
35
|
schema: Schema.Schema.AnyNoContext;
|
|
34
36
|
view: DataType.View;
|
|
37
|
+
kind?: 'basic' | 'advanced';
|
|
35
38
|
registry?: SchemaRegistry;
|
|
36
39
|
readonly?: boolean;
|
|
37
40
|
showHeading?: boolean;
|
|
38
|
-
|
|
41
|
+
onQueryChanged?: (query: string) => void;
|
|
39
42
|
onDelete?: (fieldId: string) => void;
|
|
40
43
|
} & Pick<FormProps<any>, 'outerSpacing'>
|
|
41
44
|
>;
|
|
@@ -47,37 +50,52 @@ export const ViewEditor = ({
|
|
|
47
50
|
classNames,
|
|
48
51
|
schema,
|
|
49
52
|
view,
|
|
53
|
+
kind = 'basic',
|
|
50
54
|
registry,
|
|
51
|
-
readonly
|
|
55
|
+
readonly,
|
|
52
56
|
showHeading = false,
|
|
53
|
-
|
|
57
|
+
onQueryChanged,
|
|
54
58
|
onDelete,
|
|
55
59
|
outerSpacing = true,
|
|
56
60
|
}: ViewEditorProps) => {
|
|
61
|
+
const space = getSpace(view);
|
|
62
|
+
const schemaReadonly = !isMutable(schema);
|
|
57
63
|
const { t } = useTranslation(translationKey);
|
|
58
|
-
const
|
|
64
|
+
const projectionModel = useMemo(() => {
|
|
59
65
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
60
66
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
61
67
|
return new ProjectionModel(jsonSchema, view.projection);
|
|
62
68
|
}, [schema, view.projection]);
|
|
63
|
-
const [
|
|
64
|
-
|
|
69
|
+
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
70
|
+
|
|
71
|
+
const serializedQuery = Match.value(kind).pipe(
|
|
72
|
+
Match.when('basic', () => typenameFromQuery(view.query)),
|
|
73
|
+
Match.when('advanced', () => {
|
|
74
|
+
if (view.query.type !== 'select') {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const serializer = new QuerySerializer();
|
|
79
|
+
return serializer.serialize(createExpression(view.query.filter));
|
|
80
|
+
}),
|
|
81
|
+
Match.exhaustive,
|
|
82
|
+
);
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
73
|
-
}, [
|
|
84
|
+
const viewSchema = useMemo(() => {
|
|
85
|
+
return Schema.Struct({
|
|
86
|
+
query:
|
|
87
|
+
kind === 'basic'
|
|
88
|
+
? Format.URL.annotations({ title: 'Record type' })
|
|
89
|
+
: Schema.String.annotations({ title: 'Query' }),
|
|
90
|
+
}).pipe(Schema.mutable);
|
|
91
|
+
}, [kind]);
|
|
92
|
+
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
93
|
+
// TODO(burdon): Settings should have domain name owned by user.
|
|
94
|
+
const viewValues = useMemo(() => ({ query: serializedQuery }), [serializedQuery]);
|
|
74
95
|
|
|
75
|
-
const
|
|
96
|
+
const handleToggleField = useCallback(
|
|
76
97
|
(field: FieldType) => {
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
setField((f) => (f === field ? undefined : field));
|
|
98
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
81
99
|
},
|
|
82
100
|
[readonly],
|
|
83
101
|
);
|
|
@@ -86,32 +104,32 @@ export const ViewEditor = ({
|
|
|
86
104
|
|
|
87
105
|
const handleAdd = useCallback(() => {
|
|
88
106
|
invariant(!readonly);
|
|
89
|
-
const field =
|
|
90
|
-
|
|
91
|
-
}, [schema,
|
|
107
|
+
const field = projectionModel.createFieldProjection();
|
|
108
|
+
setExpandedField(field.id);
|
|
109
|
+
}, [schema, projectionModel, readonly]);
|
|
92
110
|
|
|
93
111
|
const handleUpdate = useCallback(
|
|
94
|
-
({
|
|
112
|
+
({ query }: Schema.Schema.Type<typeof viewSchema>) => {
|
|
95
113
|
invariant(!readonly);
|
|
96
114
|
requestAnimationFrame(() => {
|
|
97
|
-
if (
|
|
98
|
-
|
|
115
|
+
if (serializedQuery !== query && !readonly) {
|
|
116
|
+
onQueryChanged?.(query);
|
|
99
117
|
}
|
|
100
118
|
});
|
|
101
119
|
},
|
|
102
|
-
[
|
|
120
|
+
[serializedQuery, onQueryChanged, readonly],
|
|
103
121
|
);
|
|
104
122
|
|
|
105
123
|
const handleDelete = useCallback(
|
|
106
124
|
(fieldId: string) => {
|
|
107
125
|
invariant(!readonly);
|
|
108
|
-
if (fieldId ===
|
|
109
|
-
|
|
126
|
+
if (fieldId === expandedField) {
|
|
127
|
+
setExpandedField(undefined);
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
onDelete?.(fieldId);
|
|
113
131
|
},
|
|
114
|
-
[
|
|
132
|
+
[expandedField, onDelete, readonly],
|
|
115
133
|
);
|
|
116
134
|
|
|
117
135
|
const handleMove = useCallback(
|
|
@@ -126,134 +144,135 @@ export const ViewEditor = ({
|
|
|
126
144
|
[view.projection.fields, readonly],
|
|
127
145
|
);
|
|
128
146
|
|
|
129
|
-
const handleClose = useCallback(() =>
|
|
130
|
-
|
|
131
|
-
const hiddenProperties = projection.getHiddenProperties();
|
|
147
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
132
148
|
|
|
133
149
|
const handleHide = useCallback(
|
|
134
150
|
(fieldId: string) => {
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
setExpandedField(undefined);
|
|
152
|
+
projectionModel.hideFieldProjection(fieldId);
|
|
137
153
|
},
|
|
138
|
-
[
|
|
154
|
+
[projectionModel],
|
|
139
155
|
);
|
|
140
156
|
|
|
141
157
|
const handleShow = useCallback(
|
|
142
158
|
(property: string) => {
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
setExpandedField(undefined);
|
|
160
|
+
projectionModel.showFieldProjection(property as JsonProp);
|
|
145
161
|
},
|
|
146
|
-
[
|
|
162
|
+
[projectionModel],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const custom: Partial<Record<string, InputComponent>> = useMemo(
|
|
166
|
+
() =>
|
|
167
|
+
kind === 'advanced'
|
|
168
|
+
? {
|
|
169
|
+
['query' satisfies keyof Schema.Schema.Type<typeof viewSchema>]: (props: InputProps) => {
|
|
170
|
+
const handleChange = useCallback(
|
|
171
|
+
(text: string) => props.onValueChange('string', text),
|
|
172
|
+
[props.onValueChange],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// TODO(wittjosiah): Add label & input styles.
|
|
176
|
+
return <QueryEditor space={space} initialValue={props.getValue()} onChange={handleChange} />;
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
: {},
|
|
180
|
+
[],
|
|
147
181
|
);
|
|
148
182
|
|
|
149
183
|
return (
|
|
150
184
|
<div role='none' className={mx(classNames)}>
|
|
151
|
-
{
|
|
152
|
-
<
|
|
153
|
-
<Callout.
|
|
154
|
-
|
|
155
|
-
</Callout.Root>
|
|
156
|
-
</div>
|
|
185
|
+
{schemaReadonly && (
|
|
186
|
+
<Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
|
|
187
|
+
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
188
|
+
</Callout.Root>
|
|
157
189
|
)}
|
|
158
190
|
|
|
159
191
|
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
160
192
|
{/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
|
|
161
|
-
<Form<
|
|
193
|
+
<Form<Schema.Schema.Type<typeof viewSchema>>
|
|
162
194
|
autoSave
|
|
163
|
-
schema={
|
|
195
|
+
schema={viewSchema}
|
|
164
196
|
values={viewValues}
|
|
165
|
-
readonly={readonly}
|
|
197
|
+
readonly={readonly ? 'disabled-input' : false}
|
|
166
198
|
onSave={handleUpdate}
|
|
167
199
|
outerSpacing={outerSpacing}
|
|
200
|
+
Custom={custom}
|
|
168
201
|
/>
|
|
169
202
|
|
|
170
203
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
171
|
-
<
|
|
204
|
+
<h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
|
|
172
205
|
|
|
173
206
|
<List.Root<FieldType>
|
|
174
207
|
items={view.projection.fields}
|
|
175
208
|
isItem={Schema.is(FieldSchema)}
|
|
176
209
|
getId={(field) => field.id}
|
|
177
|
-
readonly={readonly}
|
|
178
210
|
onMove={readonly ? undefined : handleMove}
|
|
211
|
+
readonly={readonly}
|
|
179
212
|
>
|
|
180
213
|
{({ items: fields }) => (
|
|
181
214
|
<>
|
|
182
|
-
{showHeading && (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
{showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
|
|
216
|
+
<div role='list' className={listGrid}>
|
|
217
|
+
{fields?.map((field) => {
|
|
218
|
+
const hidden = field.visible === false;
|
|
219
|
+
return (
|
|
220
|
+
<List.Item<FieldType>
|
|
221
|
+
key={field.id}
|
|
222
|
+
item={field}
|
|
223
|
+
classNames={listItemGrid}
|
|
224
|
+
aria-expanded={expandedField === field.id}
|
|
225
|
+
>
|
|
226
|
+
<div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
|
|
227
|
+
<List.ItemDragHandle disabled={readonly} />
|
|
228
|
+
<List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
|
|
229
|
+
{field.path}
|
|
230
|
+
</List.ItemTitle>
|
|
231
|
+
<List.ItemButton
|
|
232
|
+
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
233
|
+
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
234
|
+
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
235
|
+
autoHide={false}
|
|
236
|
+
disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
|
|
237
|
+
onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
|
|
238
|
+
/>
|
|
239
|
+
{/* TODO(burdon): Remove unless implement undo. */}
|
|
240
|
+
<List.ItemDeleteButton
|
|
241
|
+
label={t('delete field label')}
|
|
242
|
+
autoHide={false}
|
|
243
|
+
disabled={readonly || schemaReadonly || view.projection.fields.length <= 1}
|
|
244
|
+
onClick={() => handleDelete(field.id)}
|
|
245
|
+
data-testid='field.delete'
|
|
246
|
+
/>
|
|
247
|
+
<IconButton
|
|
248
|
+
iconOnly
|
|
249
|
+
variant='ghost'
|
|
250
|
+
label={t('toggle expand label', { ns: 'os' })}
|
|
251
|
+
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
252
|
+
onClick={() => handleToggleField(field)}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
{expandedField === field.id && (
|
|
256
|
+
<div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
|
|
257
|
+
<FieldEditor
|
|
258
|
+
readonly={readonly || schemaReadonly ? 'disabled-input' : false}
|
|
259
|
+
projection={projectionModel}
|
|
260
|
+
field={field}
|
|
261
|
+
registry={registry}
|
|
262
|
+
onSave={handleClose}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</List.Item>
|
|
267
|
+
);
|
|
268
|
+
})}
|
|
214
269
|
</div>
|
|
215
270
|
</>
|
|
216
271
|
)}
|
|
217
272
|
</List.Root>
|
|
218
|
-
|
|
219
|
-
{hiddenProperties.length > 0 && (
|
|
220
|
-
<>
|
|
221
|
-
<label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
|
|
222
|
-
|
|
223
|
-
<List.Root<string>
|
|
224
|
-
items={hiddenProperties}
|
|
225
|
-
isItem={(item): item is string => typeof item === 'string'}
|
|
226
|
-
getId={(property) => property}
|
|
227
|
-
>
|
|
228
|
-
{({ items: properties }) => (
|
|
229
|
-
<div role='list' className='flex flex-col is-full'>
|
|
230
|
-
{properties?.map((property) => (
|
|
231
|
-
<List.Item<string>
|
|
232
|
-
key={property}
|
|
233
|
-
item={property}
|
|
234
|
-
classNames={mx(grid, ghostHover, !readonly && 'cursor-pointer')}
|
|
235
|
-
>
|
|
236
|
-
<List.ItemDragHandle disabled />
|
|
237
|
-
<List.ItemTitle>{property}</List.ItemTitle>
|
|
238
|
-
<List.ItemButton
|
|
239
|
-
icon='ph--eye--regular'
|
|
240
|
-
onClick={() => handleShow(property)}
|
|
241
|
-
data-testid='show-field-button'
|
|
242
|
-
/>
|
|
243
|
-
</List.Item>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
246
|
-
)}
|
|
247
|
-
</List.Root>
|
|
248
|
-
</>
|
|
249
|
-
)}
|
|
250
273
|
</div>
|
|
251
274
|
|
|
252
|
-
{
|
|
253
|
-
<FieldEditor key={field.id} projection={projection} field={field} registry={registry} onSave={handleClose} />
|
|
254
|
-
)}
|
|
255
|
-
|
|
256
|
-
{!readonly && !field && (
|
|
275
|
+
{!readonly && !expandedField && (
|
|
257
276
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
258
277
|
<IconButton
|
|
259
278
|
icon='ph--plus--regular'
|
|
@@ -261,7 +280,7 @@ export const ViewEditor = ({
|
|
|
261
280
|
onClick={readonly ? undefined : handleAdd}
|
|
262
281
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
263
282
|
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
264
|
-
classNames='
|
|
283
|
+
classNames='is-full'
|
|
265
284
|
/>
|
|
266
285
|
</div>
|
|
267
286
|
)}
|
package/src/components/index.ts
CHANGED
|
@@ -9,16 +9,22 @@ import { Card } from '@dxos/react-ui-stack';
|
|
|
9
9
|
import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
|
|
10
10
|
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
<div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
|
|
14
|
-
<div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
|
|
15
|
-
<JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
|
|
16
|
-
</div>
|
|
17
|
-
);
|
|
12
|
+
type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
|
|
18
13
|
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
|
|
17
|
+
<div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
|
|
18
|
+
<JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
24
|
+
|
|
25
|
+
export const TestPanel = ({ classNames, children }: TestPanelProps) => {
|
|
26
|
+
return <Card.StaticRoot classNames={[textBlockWidth, classNames]}>{children}</Card.StaticRoot>;
|
|
27
|
+
};
|
|
22
28
|
|
|
23
29
|
// Symbol for accessing debug objects in tests.
|
|
24
30
|
export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
|