@dxos/react-ui-form 0.8.4-main.d05539e30a → 0.8.4-main.d9fc60f731
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 +1335 -561
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +2 -1
- package/dist/lib/browser/translations.mjs.map +2 -2
- package/dist/lib/node-esm/index.mjs +1335 -561
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +2 -1
- package/dist/lib/node-esm/translations.mjs.map +2 -2
- 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 +2 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +35 -9
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +76 -16
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +14 -4
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +22 -10
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +8 -2
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
- package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
- package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +119 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
- package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
- package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
- package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
- package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +2 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts +4 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +2 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/index.d.ts +2 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts +3 -0
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
- package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
- package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
- package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +3 -4
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +2 -2
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +2 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +2 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
- package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +2 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +7 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +4 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +9 -0
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +11 -0
- package/dist/types/src/util/properties.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -32
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.test.tsx +4 -1
- package/src/components/FieldEditor/FieldEditor.tsx +19 -14
- package/src/components/Form/Form.stories.tsx +21 -27
- package/src/components/Form/Form.tsx +106 -19
- package/src/components/Form/FormField.tsx +85 -28
- package/src/components/Form/FormFieldComponent.tsx +75 -20
- package/src/components/Form/FormFieldSet.tsx +32 -3
- package/src/components/Form/FormTooltipsContext.ts +25 -0
- package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
- package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
- package/src/components/Form/Layout/FormLayout.test.ts +59 -0
- package/src/components/Form/Layout/FormLayout.tsx +251 -0
- package/src/components/Form/Layout/annotation.ts +38 -0
- package/src/components/Form/Layout/index.ts +7 -0
- package/src/components/Form/Layout/parser.test.ts +120 -0
- package/src/components/Form/Layout/parser.ts +167 -0
- package/src/components/Form/fields/ArrayField.tsx +61 -38
- package/src/components/Form/fields/DateField.tsx +101 -0
- package/src/components/Form/fields/GeoPointField.tsx +4 -1
- package/src/components/Form/fields/InlineRefField.tsx +146 -0
- package/src/components/Form/fields/MarkdownField.tsx +3 -3
- package/src/components/Form/fields/RefField.tsx +29 -23
- package/src/components/Form/fields/SelectField.tsx +17 -5
- package/src/components/Form/fields/SelectOptionField.tsx +4 -3
- package/src/components/Form/fields/TextAreaField.tsx +1 -1
- package/src/components/Form/fields/TupleField.tsx +2 -1
- package/src/components/Form/fields/index.ts +2 -0
- package/src/components/Form/index.ts +3 -0
- package/src/components/Form/meta-tags.test.ts +47 -0
- package/src/components/Form/meta-tags.ts +33 -0
- package/src/components/ObjectForm/ObjectForm.tsx +21 -26
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +2 -2
- package/src/components/ObjectPicker/ObjectPicker.tsx +2 -2
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +14 -26
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +20 -10
- package/src/components/ObjectProperties/ObjectProperties.tsx +26 -30
- package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
- package/src/components/ObjectTree/ObjectTree.tsx +190 -0
- package/src/components/ObjectTree/index.ts +5 -0
- package/src/components/Settings/Settings.tsx +2 -1
- package/src/components/Settings/SettingsItem.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +35 -40
- package/src/components/ViewEditor/ViewEditor.test.tsx +11 -8
- package/src/components/ViewEditor/ViewEditor.tsx +29 -32
- package/src/components/index.ts +1 -0
- package/src/components/testing.tsx +4 -4
- package/src/translations.ts +2 -1
- package/src/util/properties.test.ts +67 -9
- package/src/util/properties.ts +36 -4
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { parseProto } from '@dxos/effect-proto';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
10
|
+
import { type Config as ConfigProto, Runtime } from '@dxos/protocols/proto/dxos/config';
|
|
11
|
+
// Raw .proto source resolved through `@dxos/protocols`'s `./proto/dxos/*.proto`
|
|
12
|
+
// exports entry. Vite's `?raw` query suffix returns the file as a string so we
|
|
13
|
+
// can hand it straight to `parseProto` -- the same flow the unit test uses.
|
|
14
|
+
import configProto from '@dxos/protocols/proto/dxos/config.proto?raw';
|
|
15
|
+
import { Syntax } from '@dxos/react-ui-syntax-highlighter';
|
|
16
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
17
|
+
|
|
18
|
+
import { translations } from '#translations';
|
|
19
|
+
|
|
20
|
+
import { Form } from '../Form';
|
|
21
|
+
import { ObjectTree } from './ObjectTree';
|
|
22
|
+
|
|
23
|
+
const registry = parseProto(configProto);
|
|
24
|
+
const ConfigSchema = registry.get('dxos.config.Config');
|
|
25
|
+
|
|
26
|
+
// A representative Config value that exercises nested messages, arrays, enums
|
|
27
|
+
// (rendered as their string names by the proto codec), and `google.protobuf.Any`.
|
|
28
|
+
const value: ConfigProto = {
|
|
29
|
+
version: 1,
|
|
30
|
+
runtime: {
|
|
31
|
+
client: {
|
|
32
|
+
log: { filter: 'info', prefix: 'app' },
|
|
33
|
+
storage: {
|
|
34
|
+
persistent: true,
|
|
35
|
+
sqliteMode: Runtime.Client.Storage.SqliteMode.OPFS,
|
|
36
|
+
dataRoot: '/var/lib/dxos',
|
|
37
|
+
},
|
|
38
|
+
edgeFeatures: {
|
|
39
|
+
echoReplicator: true,
|
|
40
|
+
subductionReplicator: true,
|
|
41
|
+
signaling: true,
|
|
42
|
+
},
|
|
43
|
+
servicesMode: Runtime.Client.ServicesMode.SHARED_WORKER,
|
|
44
|
+
enableSnapshots: false,
|
|
45
|
+
snapshotInterval: 60_000,
|
|
46
|
+
},
|
|
47
|
+
app: {
|
|
48
|
+
org: 'DXOS',
|
|
49
|
+
theme: 'dark',
|
|
50
|
+
website: 'https://dxos.org',
|
|
51
|
+
build: {
|
|
52
|
+
timestamp: '2026-05-22T12:00:00Z',
|
|
53
|
+
commitHash: 'abc1234',
|
|
54
|
+
version: '0.8.3',
|
|
55
|
+
branch: 'main',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
services: {
|
|
59
|
+
edge: { url: 'wss://edge.dxos.org' },
|
|
60
|
+
signaling: [
|
|
61
|
+
{ server: 'wss://signal-1.dxos.org', api: 'v1' },
|
|
62
|
+
{ server: 'wss://signal-2.dxos.org', api: 'v1' },
|
|
63
|
+
],
|
|
64
|
+
iceProviders: [{ urls: 'turn:turn.dxos.org' }],
|
|
65
|
+
},
|
|
66
|
+
keys: [
|
|
67
|
+
{ name: 'OPENAI_API_KEY', value: 'sk-...' },
|
|
68
|
+
{ name: 'ANTHROPIC_API_KEY', value: 'ant-...' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const meta = {
|
|
74
|
+
title: 'ui/react-ui-form/ObjectTree',
|
|
75
|
+
component: ObjectTree,
|
|
76
|
+
decorators: [withTheme(), withLayout({ layout: 'column', scroll: true })],
|
|
77
|
+
parameters: {
|
|
78
|
+
layout: 'fullscreen',
|
|
79
|
+
translations,
|
|
80
|
+
},
|
|
81
|
+
} satisfies Meta<typeof ObjectTree>;
|
|
82
|
+
|
|
83
|
+
export default meta;
|
|
84
|
+
|
|
85
|
+
type Story = StoryObj<typeof meta>;
|
|
86
|
+
|
|
87
|
+
export const Default: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
schema: ConfigSchema,
|
|
90
|
+
value: value,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const JSON = () => (
|
|
95
|
+
<Syntax.Root data={value}>
|
|
96
|
+
<Syntax.Content>
|
|
97
|
+
<Syntax.Filter />
|
|
98
|
+
<Syntax.Viewport>
|
|
99
|
+
<Syntax.Code />
|
|
100
|
+
</Syntax.Viewport>
|
|
101
|
+
</Syntax.Content>
|
|
102
|
+
</Syntax.Root>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Renders the same proto-derived `ConfigSchema` + `value` through the
|
|
106
|
+
// interactive `Form` component, for side-by-side comparison with the
|
|
107
|
+
// read-only ObjectTree above. `db` is intentionally omitted -- the proto
|
|
108
|
+
// schema is not an ECHO type, so there's no space/database in play.
|
|
109
|
+
export const WithForm = () => (
|
|
110
|
+
<Form.Root
|
|
111
|
+
schema={ConfigSchema as any}
|
|
112
|
+
defaultValues={value as any}
|
|
113
|
+
onSave={(next) => log.info('save', { next })}
|
|
114
|
+
onCancel={() => log.info('cancel')}
|
|
115
|
+
>
|
|
116
|
+
<Form.Viewport>
|
|
117
|
+
<Form.Content>
|
|
118
|
+
<Form.FieldSet />
|
|
119
|
+
<Form.Actions />
|
|
120
|
+
</Form.Content>
|
|
121
|
+
</Form.Viewport>
|
|
122
|
+
</Form.Root>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export const WithReadOnlyForm = () => (
|
|
126
|
+
<Form.Root schema={ConfigSchema as any} defaultValues={value as any} readonly={true}>
|
|
127
|
+
<Form.Viewport>
|
|
128
|
+
<Form.Content>
|
|
129
|
+
<Form.FieldSet />
|
|
130
|
+
<Form.Actions />
|
|
131
|
+
</Form.Content>
|
|
132
|
+
</Form.Viewport>
|
|
133
|
+
</Form.Root>
|
|
134
|
+
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// A minimal, read-only schema-driven viewer. Walks an Effect Schema and a
|
|
6
|
+
// matching value and produces a flat list of indented key/value rows -- one
|
|
7
|
+
// per leaf field, with nested Struct fields contributing a header row plus
|
|
8
|
+
// recursively-indented children, and arrays expanded as `[index]` rows.
|
|
9
|
+
//
|
|
10
|
+
// Designed to drive a hierarchical editor over schemas produced by
|
|
11
|
+
// `@dxos/effect-proto` (e.g. the proto-generated `dxos.config.Config`
|
|
12
|
+
// schema), but works for any Effect Struct schema.
|
|
13
|
+
|
|
14
|
+
import * as Schema from 'effect/Schema';
|
|
15
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
16
|
+
import React, { type ReactElement, type ReactNode, type RefAttributes } from 'react';
|
|
17
|
+
|
|
18
|
+
import { composable, composableProps } from '@dxos/react-ui';
|
|
19
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
|
+
import { type ComposableProps } from '@dxos/ui-types';
|
|
21
|
+
|
|
22
|
+
export type ObjectTreeProps<T> = {
|
|
23
|
+
schema: Schema.Schema<T, any, never>;
|
|
24
|
+
value: T;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render a value as a sequence of key/value rows whose shape and order are
|
|
29
|
+
* driven by `schema`. Object-typed properties become a header row whose
|
|
30
|
+
* children are indented under it; arrays expand element-by-element.
|
|
31
|
+
*
|
|
32
|
+
* Composable: spreads unknown props (incl. `className` / `classNames` /
|
|
33
|
+
* `style` / `role`) onto its root `<div>` and forwards its ref, so it can
|
|
34
|
+
* be slotted into themed parents like `Panel.Content`.
|
|
35
|
+
*/
|
|
36
|
+
const ObjectTreeImpl = composable<HTMLDivElement, ObjectTreeProps<any>>(({ schema, value, ...props }, forwardedRef) => (
|
|
37
|
+
<div {...composableProps(props, { role: 'table', classNames: 'font-mono text-sm w-full' })} ref={forwardedRef}>
|
|
38
|
+
<Node ast={schema.ast} value={value} label={null} depth={0} />
|
|
39
|
+
</div>
|
|
40
|
+
));
|
|
41
|
+
|
|
42
|
+
ObjectTreeImpl.displayName = 'ObjectTree';
|
|
43
|
+
|
|
44
|
+
// `composable<HTMLDivElement, ObjectTreeProps<any>>` erases the `T` type
|
|
45
|
+
// parameter inside the factory; the cast restores the generic signature for
|
|
46
|
+
// consumers without changing the underlying component (the COMPOSABLE marker
|
|
47
|
+
// stays attached). This is the pattern documented on `composable()` itself.
|
|
48
|
+
export const ObjectTree = ObjectTreeImpl as unknown as <T>(
|
|
49
|
+
props: ComposableProps<ObjectTreeProps<T>> & RefAttributes<HTMLDivElement>,
|
|
50
|
+
) => ReactElement | null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Peel wrappers that don't affect the runtime shape: Refinements pass their
|
|
54
|
+
* `from`, Suspends resolve via `f()`, and Transformations expose the encoded
|
|
55
|
+
* (input) side. Repeated unwrapping handles chains like `Schema.suspend ->
|
|
56
|
+
* Refinement -> TypeLiteral`.
|
|
57
|
+
*/
|
|
58
|
+
const unwrap = (ast: SchemaAST.AST): SchemaAST.AST => {
|
|
59
|
+
if (SchemaAST.isRefinement(ast)) {
|
|
60
|
+
return unwrap(ast.from);
|
|
61
|
+
}
|
|
62
|
+
if (SchemaAST.isSuspend(ast)) {
|
|
63
|
+
return unwrap(ast.f());
|
|
64
|
+
}
|
|
65
|
+
if (SchemaAST.isTransformation(ast)) {
|
|
66
|
+
return unwrap(ast.from);
|
|
67
|
+
}
|
|
68
|
+
return ast;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Effect models `Schema.optional(X)` as a property whose type AST is
|
|
73
|
+
* `Union(X, UndefinedKeyword)`. For walking purposes we want X -- the
|
|
74
|
+
* undefined branch carries no schema information for the viewer.
|
|
75
|
+
*/
|
|
76
|
+
const stripOptional = (ast: SchemaAST.AST): SchemaAST.AST => {
|
|
77
|
+
if (!SchemaAST.isUnion(ast)) {
|
|
78
|
+
return ast;
|
|
79
|
+
}
|
|
80
|
+
const nonUndef = ast.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
|
|
81
|
+
return nonUndef.length === 1 ? nonUndef[0] : ast;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type NodeProps = {
|
|
85
|
+
ast: SchemaAST.AST;
|
|
86
|
+
value: unknown;
|
|
87
|
+
/** Row label; `null` means render children directly without a header row. */
|
|
88
|
+
label: ReactNode | null;
|
|
89
|
+
depth: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const Node = ({ ast, value, label, depth }: NodeProps) => {
|
|
93
|
+
const inner = unwrap(stripOptional(ast));
|
|
94
|
+
|
|
95
|
+
// Struct: header row + indented children. Skipping the header at depth 0
|
|
96
|
+
// keeps the root flush-left.
|
|
97
|
+
if (SchemaAST.isTypeLiteral(inner) && isPlainObject(value)) {
|
|
98
|
+
const childDepth = label === null ? depth : depth + 1;
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
{label !== null && <Row label={label} value={null} depth={depth} kind='group' />}
|
|
102
|
+
{inner.propertySignatures.map((prop) => (
|
|
103
|
+
<Node
|
|
104
|
+
key={String(prop.name)}
|
|
105
|
+
ast={prop.type}
|
|
106
|
+
value={(value as Record<string, unknown>)[String(prop.name)]}
|
|
107
|
+
label={String(prop.name)}
|
|
108
|
+
depth={childDepth}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Array (`Schema.Array(X)` -> TupleType with single rest element).
|
|
116
|
+
if (SchemaAST.isTupleType(inner) && Array.isArray(value)) {
|
|
117
|
+
const elemType = inner.rest[0]?.type;
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
{label !== null && <Row label={label} value={`Array(${value.length})`} depth={depth} kind='group' />}
|
|
121
|
+
{value.map((item, index) => (
|
|
122
|
+
<Node
|
|
123
|
+
key={index}
|
|
124
|
+
ast={elemType ?? SchemaAST.unknownKeyword}
|
|
125
|
+
value={item}
|
|
126
|
+
label={`[${index}]`}
|
|
127
|
+
depth={depth + 1}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Leaf (scalars, unions of literals, unknown, etc.).
|
|
135
|
+
return <Row label={label ?? ''} value={formatLeaf(value)} depth={depth} kind='leaf' />;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type RowProps = {
|
|
139
|
+
label: ReactNode;
|
|
140
|
+
value: ReactNode;
|
|
141
|
+
depth: number;
|
|
142
|
+
kind: 'group' | 'leaf';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const Row = ({ label, value, depth }: RowProps) => (
|
|
146
|
+
<div role='row' className={mx('flex items-baseline gap-3 py-1')} style={{ paddingInlineStart: 8 + depth * 16 }}>
|
|
147
|
+
<div role='cell' className='text-subdued'>
|
|
148
|
+
{label}
|
|
149
|
+
</div>
|
|
150
|
+
<div role='cell' className={mx('break-all text-base-fg')}>
|
|
151
|
+
{value}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
157
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Format a leaf value for display. Strings are quoted so empty strings are
|
|
161
|
+
* visible; missing values surface as an em-dash so the row stays present.
|
|
162
|
+
*/
|
|
163
|
+
const formatLeaf = (value: unknown): ReactNode => {
|
|
164
|
+
if (value === undefined) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (value === null) {
|
|
169
|
+
return <span className='text-description'>null</span>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
switch (typeof value) {
|
|
173
|
+
case 'string':
|
|
174
|
+
// Quote strings so empty strings remain visible (`""` rather than blank).
|
|
175
|
+
return <span className='text-blue-text'>{JSON.stringify(value)}</span>;
|
|
176
|
+
case 'number':
|
|
177
|
+
case 'bigint':
|
|
178
|
+
return <span className='text-green-text'>{String(value)}</span>;
|
|
179
|
+
case 'boolean':
|
|
180
|
+
return <span className='text-purple-text'>{String(value)}</span>;
|
|
181
|
+
default:
|
|
182
|
+
// Objects we couldn't expand via schema (e.g. `google.protobuf.Any` -> Unknown)
|
|
183
|
+
// are rendered as compact JSON.
|
|
184
|
+
try {
|
|
185
|
+
return JSON.stringify(value);
|
|
186
|
+
} catch {
|
|
187
|
+
return String(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type Label, ScrollArea, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
8
|
-
import { composable, composableProps
|
|
8
|
+
import { composable, composableProps } from '@dxos/react-ui';
|
|
9
|
+
import { mx } from '@dxos/ui-theme';
|
|
9
10
|
|
|
10
11
|
import { translationKey } from '#translations';
|
|
11
12
|
|
|
@@ -41,7 +41,7 @@ export const SettingsItem = ({ title, description = '', children }: SettingsItem
|
|
|
41
41
|
return (
|
|
42
42
|
<Input.Root>
|
|
43
43
|
<SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
|
|
44
|
-
<Input.Label classNames='pb-trim-md text-base-
|
|
44
|
+
<Input.Label classNames='pb-trim-md text-base-fg text-lg md:col-span-2'>
|
|
45
45
|
{toLocalizedString(title, t)}
|
|
46
46
|
</Input.Label>
|
|
47
47
|
<Input.DescriptionAndValidation>
|
|
@@ -6,8 +6,8 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type, type View } from '@dxos/echo';
|
|
10
|
-
import {
|
|
9
|
+
import { DXN, EID, Filter, JsonSchema, Obj, Query, type QueryAST, Scope, Tag, Type, type View } from '@dxos/echo';
|
|
10
|
+
import { Format, type Mutable } from '@dxos/echo/internal';
|
|
11
11
|
import { useQuery } from '@dxos/react-client/echo';
|
|
12
12
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
13
13
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
@@ -22,15 +22,15 @@ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
|
22
22
|
|
|
23
23
|
const types = [
|
|
24
24
|
// TODO(burdon): Get label from annotation.
|
|
25
|
-
{ value: Organization.Organization
|
|
26
|
-
{ value: Person.Person
|
|
27
|
-
{ value: Pipeline.Pipeline
|
|
28
|
-
{ value: Employer.Employer
|
|
25
|
+
{ value: Type.getTypename(Organization.Organization), label: 'Organization' },
|
|
26
|
+
{ value: Type.getTypename(Person.Person), label: 'Person' },
|
|
27
|
+
{ value: Type.getTypename(Pipeline.Pipeline), label: 'Project' },
|
|
28
|
+
{ value: Type.getTypename(Employer.Employer), label: 'Employer' },
|
|
29
29
|
];
|
|
30
30
|
|
|
31
31
|
// Type definition for debug objects exposed to tests.
|
|
32
32
|
export type ViewEditorDebugObjects = {
|
|
33
|
-
|
|
33
|
+
type: Type.AnyEntity;
|
|
34
34
|
view: View.View;
|
|
35
35
|
projection: ProjectionModel;
|
|
36
36
|
};
|
|
@@ -39,7 +39,7 @@ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
|
|
|
39
39
|
|
|
40
40
|
const DefaultStory = (props: DefaultStoryProps) => {
|
|
41
41
|
const { space } = useClientStory();
|
|
42
|
-
const [
|
|
42
|
+
const [type, setType] = useState<Type.AnyEntity>();
|
|
43
43
|
const [view, setView] = useState<View.View>();
|
|
44
44
|
const projectionRef = useRef<ProjectionModel>(null);
|
|
45
45
|
|
|
@@ -51,71 +51,66 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
51
51
|
name: Schema.String,
|
|
52
52
|
email: Format.Email,
|
|
53
53
|
salary: Format.Currency(),
|
|
54
|
-
}).pipe(
|
|
55
|
-
Type.object({
|
|
56
|
-
typename: 'com.example.type.test',
|
|
57
|
-
version: '0.1.0',
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
54
|
+
}).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
|
|
60
55
|
|
|
61
56
|
const AlternateSchema = Schema.Struct({
|
|
62
57
|
title: Schema.String,
|
|
63
58
|
description: Schema.String,
|
|
64
59
|
completed: Schema.Boolean,
|
|
65
|
-
}).pipe(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}),
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
|
|
60
|
+
}).pipe(Type.makeObject(DXN.make('com.example.type.alternate', '0.1.0')));
|
|
61
|
+
|
|
62
|
+
const testType = await space.db.addType(TestSchema);
|
|
63
|
+
await space.db.addType(AlternateSchema);
|
|
73
64
|
const view = ViewModel.make({
|
|
74
65
|
name: 'Test',
|
|
75
66
|
query: Query.select(Filter.type(TestSchema)),
|
|
76
67
|
jsonSchema: JsonSchema.toJsonSchema(TestSchema),
|
|
77
68
|
});
|
|
78
69
|
|
|
79
|
-
|
|
70
|
+
setType(testType);
|
|
80
71
|
setView(view);
|
|
81
72
|
}
|
|
82
73
|
}, [space]);
|
|
83
74
|
|
|
84
75
|
const updateViewQuery = useCallback(
|
|
85
|
-
async (newQuery: QueryAST.Query, target?:
|
|
86
|
-
if (!
|
|
76
|
+
async (newQuery: QueryAST.Query, target?: EID.EID) => {
|
|
77
|
+
if (!type || !view || !space) {
|
|
87
78
|
return;
|
|
88
79
|
}
|
|
89
80
|
|
|
90
81
|
if (props.mode === 'tag') {
|
|
91
|
-
const queue = target
|
|
92
|
-
const query = queue
|
|
82
|
+
const queue = target;
|
|
83
|
+
const query = queue
|
|
84
|
+
? Query.fromAst(newQuery).from([
|
|
85
|
+
Scope.feed(`dxn:queue:data:${EID.getSpaceId(queue)}:${EID.getEntityId(queue)}`),
|
|
86
|
+
])
|
|
87
|
+
: Query.fromAst(newQuery);
|
|
93
88
|
Obj.update(view, (view) => {
|
|
94
89
|
view.query.ast = query.ast as Mutable<typeof query.ast>;
|
|
95
90
|
});
|
|
96
91
|
|
|
97
92
|
const typename = getTypenameFromQuery(query.ast);
|
|
98
|
-
const
|
|
93
|
+
const allTypes = space.db.graph.registry.list().filter(Type.isType);
|
|
94
|
+
const newSchema = allTypes.find((t) => Type.getTypename(t) === typename);
|
|
99
95
|
if (!newSchema) {
|
|
100
96
|
return;
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
const newView = ViewModel.make({
|
|
104
100
|
query,
|
|
105
|
-
jsonSchema: newSchema
|
|
101
|
+
jsonSchema: JsonSchema.toJsonSchema(newSchema),
|
|
106
102
|
});
|
|
107
103
|
Obj.update(view, (view) => {
|
|
108
104
|
view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
|
|
109
105
|
});
|
|
110
|
-
|
|
106
|
+
setType(() => newSchema);
|
|
111
107
|
} else {
|
|
112
108
|
Obj.update(view, (view) => {
|
|
113
109
|
view.query.ast = newQuery as Mutable<typeof newQuery>;
|
|
114
110
|
});
|
|
115
|
-
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
116
111
|
}
|
|
117
112
|
},
|
|
118
|
-
[view,
|
|
113
|
+
[view, type],
|
|
119
114
|
);
|
|
120
115
|
|
|
121
116
|
const handleDelete = useCallback(
|
|
@@ -125,22 +120,22 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
125
120
|
|
|
126
121
|
// Expose objects on window for test access.
|
|
127
122
|
useEffect(() => {
|
|
128
|
-
if (typeof window !== 'undefined' &&
|
|
123
|
+
if (typeof window !== 'undefined' && type && view && projectionRef.current) {
|
|
129
124
|
(window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
|
|
130
|
-
|
|
125
|
+
type,
|
|
131
126
|
view,
|
|
132
127
|
projection: projectionRef.current,
|
|
133
128
|
} satisfies ViewEditorDebugObjects;
|
|
134
129
|
}
|
|
135
|
-
}, [
|
|
130
|
+
}, [type, view]);
|
|
136
131
|
|
|
137
132
|
// NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
|
|
138
133
|
const json = useMemo(
|
|
139
|
-
() => JSON.parse(JSON.stringify({ schema, view, projection: projectionRef.current })),
|
|
140
|
-
[JSON.stringify(
|
|
134
|
+
() => JSON.parse(JSON.stringify({ schema: type, view, projection: projectionRef.current })),
|
|
135
|
+
[JSON.stringify(type), JSON.stringify(view)],
|
|
141
136
|
);
|
|
142
137
|
|
|
143
|
-
if (!
|
|
138
|
+
if (!type || !view) {
|
|
144
139
|
return <div />;
|
|
145
140
|
}
|
|
146
141
|
|
|
@@ -148,9 +143,9 @@ const DefaultStory = (props: DefaultStoryProps) => {
|
|
|
148
143
|
<TestLayout json={json}>
|
|
149
144
|
<ViewEditor
|
|
150
145
|
ref={projectionRef}
|
|
151
|
-
|
|
146
|
+
type={type}
|
|
152
147
|
view={view}
|
|
153
|
-
registry={space?.db.
|
|
148
|
+
registry={space?.db.graph.registry}
|
|
154
149
|
mode={props.mode}
|
|
155
150
|
readonly={props.readonly}
|
|
156
151
|
types={types}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { composeStories } from '@storybook/react';
|
|
6
|
-
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
6
|
+
import { act, cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
7
7
|
import { afterEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
|
-
import { View } from '@dxos/echo';
|
|
10
|
-
import {
|
|
9
|
+
import { Type, View } from '@dxos/echo';
|
|
10
|
+
import { isInstanceOf } from '@dxos/echo/internal';
|
|
11
11
|
import { ProjectionModel } from '@dxos/schema';
|
|
12
12
|
|
|
13
13
|
import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
@@ -19,7 +19,7 @@ const { Default } = composeStories(stories);
|
|
|
19
19
|
const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
|
|
20
20
|
const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
|
|
21
21
|
expect(debugObjects).toBeDefined();
|
|
22
|
-
expect(debugObjects.
|
|
22
|
+
expect(Type.isType(debugObjects.type)).toBe(true);
|
|
23
23
|
expect(isInstanceOf(View.View, debugObjects.view)).toBeTruthy();
|
|
24
24
|
expect(debugObjects.projection).toBeInstanceOf(ProjectionModel);
|
|
25
25
|
return debugObjects;
|
|
@@ -35,7 +35,10 @@ const waitForViewEditor = async () => {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
describe('ViewEditor', () => {
|
|
38
|
-
afterEach(() => {
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
// Flush pending React scheduler work before teardown to prevent
|
|
40
|
+
// "window is not defined" errors from setImmediate callbacks firing after happy-dom cleanup.
|
|
41
|
+
await act(async () => {});
|
|
39
42
|
cleanup();
|
|
40
43
|
});
|
|
41
44
|
|
|
@@ -68,7 +71,7 @@ describe('ViewEditor', () => {
|
|
|
68
71
|
const debugObjects = getViewEditorDebugObjects();
|
|
69
72
|
|
|
70
73
|
// Check schema contains new_property.
|
|
71
|
-
const schemaProperties = debugObjects.
|
|
74
|
+
const schemaProperties = debugObjects.type.jsonSchema.properties;
|
|
72
75
|
expect(schemaProperties).toBeDefined();
|
|
73
76
|
expect(schemaProperties!.new_property).toBeDefined();
|
|
74
77
|
expect(schemaProperties!.name).toBeUndefined();
|
|
@@ -116,7 +119,7 @@ describe('ViewEditor', () => {
|
|
|
116
119
|
const debugObjects = getViewEditorDebugObjects();
|
|
117
120
|
|
|
118
121
|
// Check schema contains added_property.
|
|
119
|
-
const schemaProperties = debugObjects.
|
|
122
|
+
const schemaProperties = debugObjects.type.jsonSchema.properties;
|
|
120
123
|
expect(schemaProperties).toBeDefined();
|
|
121
124
|
expect(schemaProperties!.added_property).toBeDefined();
|
|
122
125
|
|
|
@@ -160,7 +163,7 @@ describe('ViewEditor', () => {
|
|
|
160
163
|
const debugObjects = getViewEditorDebugObjects();
|
|
161
164
|
|
|
162
165
|
// Check schema no longer contains the name property.
|
|
163
|
-
const schemaProperties = debugObjects.
|
|
166
|
+
const schemaProperties = debugObjects.type.jsonSchema.properties;
|
|
164
167
|
expect(schemaProperties).toBeDefined();
|
|
165
168
|
expect(schemaProperties!.name).toBeUndefined();
|
|
166
169
|
|