@dxos/react-ui-form 0.9.1-staging.ee54ba693a → 0.11.0
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/index.mjs +4064 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/translations.mjs +77 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +0 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Card.stories.d.ts +88 -0
- package/dist/types/src/components/Form/Card.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.d.ts +7 -7
- package/dist/types/src/components/Form/Form.stories.d.ts +21 -0
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.theme.d.ts +6 -0
- package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormControls.d.ts +7 -7
- package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormRow.d.ts +17 -3
- package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts +0 -6
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts +7 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +0 -11
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts +1 -11
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts +13 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +1 -13
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts +15 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +0 -27
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts +29 -0
- package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +10 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +8 -23
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -0
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +9 -8
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +6 -2
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.test.d.ts +2 -0
- package/dist/types/src/hooks/useFormHandler.test.d.ts.map +1 -0
- package/dist/types/src/testing/TestLayout.d.ts +1 -1
- package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +5 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -42
- package/src/components/Form/Card.stories.tsx +93 -0
- package/src/components/Form/Form.stories.tsx +95 -3
- package/src/components/Form/Form.theme.ts +14 -8
- package/src/components/Form/Form.tsx +2 -2
- package/src/components/Form/FormControls.tsx +32 -13
- package/src/components/Form/FormField/FormField.tsx +11 -3
- package/src/components/Form/FormField/FormRow.tsx +20 -1
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +3 -46
- package/src/components/Form/FormField/fields/ArrayField/default-value.ts +49 -0
- package/src/components/Form/FormField/fields/ArrayField/index.ts +2 -0
- package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +12 -2
- package/src/components/Form/FormField/fields/DateField/DateField.tsx +10 -3
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +1 -1
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +2 -12
- package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +1 -1
- package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +1 -36
- package/src/components/Form/FormField/fields/NumberField/index.ts +2 -0
- package/src/components/Form/FormField/fields/NumberField/numeric-constraints.ts +43 -0
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +1 -0
- package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +2 -2
- package/src/components/Form/FormField/fields/RefField/RefField.test.ts +1 -1
- package/src/components/Form/FormField/fields/RefField/RefField.tsx +3 -45
- package/src/components/Form/FormField/fields/RefField/find-ref-option.ts +56 -0
- package/src/components/Form/FormField/fields/RefField/index.ts +2 -0
- package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +2 -67
- package/src/components/Form/FormField/fields/SelectField/create-select-field.tsx +80 -0
- package/src/components/Form/FormField/fields/SelectField/index.ts +2 -0
- package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +2 -1
- package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +11 -9
- package/src/components/Form/FormLayout/FormLayout.test.ts +1 -1
- package/src/components/Form/FormLayout/FormLayout.tsx +1 -86
- package/src/components/Form/FormLayout/index.ts +2 -0
- package/src/components/Form/FormLayout/resolve-layout-field.ts +98 -0
- package/src/components/ObjectForm/ObjectForm.stories.tsx +93 -3
- package/src/components/ObjectForm/ObjectForm.tsx +22 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.tsx +3 -3
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +7 -1
- package/src/components/ObjectTree/ObjectTree.stories.tsx +1 -1
- package/src/components/RefEditor/RefEditor.stories.tsx +9 -1
- package/src/components/RefEditor/RefEditor.tsx +42 -6
- package/src/components/RefEditor/ref-editor-extension.ts +168 -91
- package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.tsx +9 -9
- package/src/hooks/useFormHandler.test.ts +85 -0
- package/src/hooks/useFormHandler.ts +118 -31
- package/src/translations.ts +1 -0
- package/src/types.ts +5 -0
- package/src/util/index.ts +0 -1
- package/dist/lib/browser/index.mjs +0 -4675
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/translations.mjs +0 -87
- package/dist/lib/browser/translations.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4677
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/translations.mjs +0 -89
- package/dist/lib/node-esm/translations.mjs.map +0 -7
- package/dist/types/src/util/formatting.d.ts +0 -15
- package/dist/types/src/util/formatting.d.ts.map +0 -1
- package/src/util/formatting.ts +0 -171
|
@@ -0,0 +1,4064 @@
|
|
|
1
|
+
import "./types.mjs";
|
|
2
|
+
import { ANY_OBJECT_TYPENAME, ReferenceAnnotationId, createAnnotationHelper } from "@dxos/echo/internal";
|
|
3
|
+
import { Children, Component, Fragment, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Annotation, DXN, EID, Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, Scope, Tag, Type, View } from "@dxos/echo";
|
|
5
|
+
import { Format as Format$1, FormatEnums, GeoLocation, TypeEnum, formatToType } from "@dxos/echo/Format";
|
|
6
|
+
import { log } from "@dxos/log";
|
|
7
|
+
import { Button, Column, Icon, IconButton, Input, Message, Popover, ScrollArea, Select, Tag as Tag$1, ToggleIconButton, Tooltip, composable, composableProps, useAsyncEffect, useDefaultValue, useInColumn, useMergeRefs, useThemeContext, useTranslation, withColumn } from "@dxos/react-ui";
|
|
8
|
+
import { FactoryAnnotation, ParentLabelAnnotation, ParentLabelAnnotationId, ProjectionModel, Text, VIEW_FIELD_LIMIT, createEchoChangeCallback, formatToAdditionalPropertyAttributes, getFormatSchema, getTypeURIFromQuery, validateSchema } from "@dxos/schema";
|
|
9
|
+
import { translationKey } from "#translations";
|
|
10
|
+
import * as Option from "effect/Option";
|
|
11
|
+
import * as Schema from "effect/Schema";
|
|
12
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
13
|
+
import { SchemaEx } from "@dxos/effect";
|
|
14
|
+
import { DEFAULT_LAYOUT_NAME, FormLayoutAnnotation, ReferenceAnnotationId as ReferenceAnnotationId$1 } from "@dxos/echo/Annotation";
|
|
15
|
+
import { MarkdownView } from "@dxos/react-ui-markdown";
|
|
16
|
+
import { getHashHue, getStyles, mx, osTranslations, tv } from "@dxos/ui-theme";
|
|
17
|
+
import * as Cause from "effect/Cause";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as Exit from "effect/Exit";
|
|
20
|
+
import { createContext } from "@radix-ui/react-context";
|
|
21
|
+
import * as Equal from "effect/Equal";
|
|
22
|
+
import * as Utils from "effect/Utils";
|
|
23
|
+
import { addEventListener } from "@dxos/async";
|
|
24
|
+
import { useObject, useQuery, useType } from "@dxos/echo-react";
|
|
25
|
+
import { DXN as DXN$1, EID as EID$1, PublicKey, URI } from "@dxos/keys";
|
|
26
|
+
import { Combobox, OrderedList } from "@dxos/react-ui-list";
|
|
27
|
+
import { arrayMove, safeParseFloat } from "@dxos/util";
|
|
28
|
+
import * as Match from "effect/Match";
|
|
29
|
+
import * as String$1 from "effect/String";
|
|
30
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
31
|
+
import { format } from "date-fns";
|
|
32
|
+
import { invariant } from "@dxos/invariant";
|
|
33
|
+
import { RegistryContext, useAtomValue } from "@effect-atom/atom-react";
|
|
34
|
+
import { Doc } from "@dxos/echo-doc";
|
|
35
|
+
import { Editor, useBasicMarkdownExtensions } from "@dxos/react-ui-editor";
|
|
36
|
+
import { createBasicExtensions, createDataExtensions, createThemeExtensions, insertAtCursor, keymap } from "@dxos/ui-editor";
|
|
37
|
+
import "@dxos/lit-ui/dx-tag-picker.pcss";
|
|
38
|
+
import { DxAnchor } from "@dxos/lit-ui/react";
|
|
39
|
+
import { useSearchListResults } from "@dxos/react-ui-search";
|
|
40
|
+
import { HuePicker } from "@dxos/react-ui-pickers";
|
|
41
|
+
import { hues } from "@dxos/ui-types";
|
|
42
|
+
import * as Function from "effect/Function";
|
|
43
|
+
import { EditorState, Prec, RangeSetBuilder, StateEffect, StateField } from "@codemirror/state";
|
|
44
|
+
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
|
|
45
|
+
import { Domino } from "@dxos/ui";
|
|
46
|
+
import { QueryForm } from "@dxos/react-ui-components";
|
|
47
|
+
//#region src/annotations.ts
|
|
48
|
+
var OptionsLookupAnnotationId = Symbol.for("@dxos/schema/annotation/OptionsLookup");
|
|
49
|
+
var OptionsLookupAnnotation = createAnnotationHelper(OptionsLookupAnnotationId);
|
|
50
|
+
/**
|
|
51
|
+
* Builds an {@link OptionsLookup} typed against a schema's value type `Values`: `deps` is checked
|
|
52
|
+
* against its field names, and `load` receives only those fields, narrowed. Pass `{ combobox: true }` to
|
|
53
|
+
* render an editable combobox.
|
|
54
|
+
*/
|
|
55
|
+
var optionsLookup = () => (deps, load, options) => ({
|
|
56
|
+
deps,
|
|
57
|
+
load,
|
|
58
|
+
combobox: options?.combobox
|
|
59
|
+
});
|
|
60
|
+
var AutofillAnnotationId = Symbol.for("@dxos/schema/annotation/Autofill");
|
|
61
|
+
var AutofillAnnotation = createAnnotationHelper(AutofillAnnotationId);
|
|
62
|
+
/** Builds an {@link Autofill} typed against a schema's value type `Values` (see {@link optionsLookup}). */
|
|
63
|
+
var autofill = () => (deps, derive) => ({
|
|
64
|
+
deps,
|
|
65
|
+
derive
|
|
66
|
+
});
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/util/field.ts
|
|
69
|
+
var parseValue = ({ type, format, value }) => {
|
|
70
|
+
if (value === void 0 || value === null || value === "") return;
|
|
71
|
+
const parseScalar = (type) => {
|
|
72
|
+
switch (type) {
|
|
73
|
+
case TypeEnum.Boolean:
|
|
74
|
+
if (typeof value === "string") {
|
|
75
|
+
const lowercaseValue = value.toLowerCase();
|
|
76
|
+
if (lowercaseValue === "0" || lowercaseValue === "false") return false;
|
|
77
|
+
else if (lowercaseValue === "1" || lowercaseValue === "true") return true;
|
|
78
|
+
}
|
|
79
|
+
return Boolean(value);
|
|
80
|
+
case TypeEnum.Number: {
|
|
81
|
+
const cleaned = typeof value === "string" ? value.replace(/[^0-9.-]+/g, "") : value;
|
|
82
|
+
const num = Number(cleaned);
|
|
83
|
+
return Number.isNaN(num) ? null : num;
|
|
84
|
+
}
|
|
85
|
+
case TypeEnum.String:
|
|
86
|
+
case TypeEnum.Ref: return String(value);
|
|
87
|
+
default: return value;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
if (!format) return parseScalar(type);
|
|
91
|
+
switch (format) {
|
|
92
|
+
case Format$1.TypeFormat.Boolean: return parseScalar(TypeEnum.Boolean);
|
|
93
|
+
case Format$1.TypeFormat.Number:
|
|
94
|
+
case Format$1.TypeFormat.Percent:
|
|
95
|
+
case Format$1.TypeFormat.Currency: return parseScalar(TypeEnum.Number);
|
|
96
|
+
case Format$1.TypeFormat.String:
|
|
97
|
+
case Format$1.TypeFormat.Markdown: return parseScalar(TypeEnum.String);
|
|
98
|
+
case Format$1.TypeFormat.Ref: throw new Error(`unexpected format: ${Format$1.TypeFormat.Ref}`);
|
|
99
|
+
case Format$1.TypeFormat.DateTime:
|
|
100
|
+
case Format$1.TypeFormat.Date:
|
|
101
|
+
case Format$1.TypeFormat.Time:
|
|
102
|
+
case Format$1.TypeFormat.Timestamp: {
|
|
103
|
+
const date = new Date(value);
|
|
104
|
+
return isNaN(date.getTime()) ? null : date;
|
|
105
|
+
}
|
|
106
|
+
case Format$1.TypeFormat.GeoPoint:
|
|
107
|
+
if (typeof value === "string") {
|
|
108
|
+
const parts = value.replace(/[^0-9.,-]+/g, "").split(",");
|
|
109
|
+
if (parts.length === 2) {
|
|
110
|
+
const lat = parseFloat(parts[0]);
|
|
111
|
+
const long = parseFloat(parts[1]);
|
|
112
|
+
if (!isNaN(lat) && !isNaN(long)) return GeoLocation.toGeoPoint({
|
|
113
|
+
latitude: lat,
|
|
114
|
+
longitude: long
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [0, 0];
|
|
119
|
+
default: return value;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var cellClassesForFieldType = ({ type, format }) => {
|
|
123
|
+
switch (format || type) {
|
|
124
|
+
case Format$1.TypeFormat.Markdown: return;
|
|
125
|
+
case Format$1.TypeFormat.Time:
|
|
126
|
+
case Format$1.TypeFormat.Timestamp:
|
|
127
|
+
case Format$1.TypeFormat.DateTime:
|
|
128
|
+
case Format$1.TypeFormat.Date:
|
|
129
|
+
case Format$1.TypeFormat.Duration: return ["font-mono", "text-right"];
|
|
130
|
+
case Format$1.TypeFormat.Currency: return ["text-right"];
|
|
131
|
+
case Format$1.TypeFormat.JSON:
|
|
132
|
+
case Format$1.TypeFormat.DID: return ["font-mono"];
|
|
133
|
+
case TypeEnum.Number:
|
|
134
|
+
case TypeEnum.Boolean: return ["text-right", "font-mono"];
|
|
135
|
+
case TypeEnum.String: return;
|
|
136
|
+
case TypeEnum.Ref: return;
|
|
137
|
+
default: return;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var pathNotUniqueError = (path) => ({
|
|
141
|
+
path: "path",
|
|
142
|
+
message: `'${path}' is already present`
|
|
143
|
+
});
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/util/omit.ts
|
|
146
|
+
var omitId = (schemaOrType) => {
|
|
147
|
+
return omitIdFromSchema(Type.isType(schemaOrType) ? Type.getSchema(schemaOrType) : schemaOrType);
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Removes the `id` field where present, preserving the schema's shape. Recurses into unions so a
|
|
151
|
+
* discriminated (create) schema keeps its discriminant — `Schema.omit('id')` is a struct operation and
|
|
152
|
+
* would otherwise flatten the union, breaking variant rendering. A no-op when there is no `id` (plain
|
|
153
|
+
* create structs that never carried one).
|
|
154
|
+
*/
|
|
155
|
+
var omitIdFromSchema = (schema) => {
|
|
156
|
+
const ast = schema.ast;
|
|
157
|
+
if (SchemaAST.isUnion(ast)) return Schema.Union(...ast.types.map((type) => omitIdFromSchema(Schema.make(type))));
|
|
158
|
+
return SchemaAST.getPropertySignatures(ast).some((prop) => prop.name === "id") ? schema.pipe(Schema.omit("id")) : schema;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Drop fields annotated with `FormInputAnnotation.set(false)` from a schema so
|
|
162
|
+
* the form's validator doesn't trip on required-but-hidden fields. Used by
|
|
163
|
+
* the picker's inline create form, where a `FactoryAnnotation` typically
|
|
164
|
+
* supplies the hidden values (e.g. a backing-object Ref) outside the form.
|
|
165
|
+
*/
|
|
166
|
+
var omitHiddenFormFields = (schema) => {
|
|
167
|
+
const hidden = SchemaAST.getPropertySignatures(schema.ast).filter((prop) => Option.getOrElse(Annotation.FormInputAnnotation.getFromAst(prop.type), () => true) === false).map((prop) => prop.name);
|
|
168
|
+
return hidden.length === 0 ? schema : schema.pipe(Schema.omit(...hidden));
|
|
169
|
+
};
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/util/properties.ts
|
|
172
|
+
/** The property's type with an optional `T | undefined` union unwrapped to its inner `T`. */
|
|
173
|
+
var unwrapOptional = (prop) => {
|
|
174
|
+
if (!prop.isOptional || !SchemaAST.isUnion(prop.type)) return prop.type;
|
|
175
|
+
const defined = prop.type.types.filter((type) => type._tag !== "UndefinedKeyword");
|
|
176
|
+
if (defined.length === 0) return prop.type;
|
|
177
|
+
return defined.length === 1 ? defined[0] : SchemaAST.Union.make(defined, prop.type.annotations);
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the properties to render for a field set's *root* schema, given the current form values.
|
|
181
|
+
*
|
|
182
|
+
* A top-level discriminated union is rendered flat: the fields of the member selected by the current
|
|
183
|
+
* discriminator value, with the discriminator field keeping the union-wide set of literals so it stays
|
|
184
|
+
* switchable. (`SchemaAST.getPropertySignatures` on a union returns only the common discriminator, so a
|
|
185
|
+
* union root would otherwise render just that one field.) Non-union roots are unchanged. Nested unions are
|
|
186
|
+
* unaffected — those are expanded per-field by `FormField`, which passes a single-member type literal here.
|
|
187
|
+
*/
|
|
188
|
+
var getRootFormProperties = (ast, values) => {
|
|
189
|
+
if (!SchemaEx.isDiscriminatedUnion(ast)) return getFormProperties(ast);
|
|
190
|
+
const activeType = SchemaEx.getDiscriminatedType(ast, values ?? {});
|
|
191
|
+
if (!activeType) return getFormProperties(ast);
|
|
192
|
+
const activeProps = getFormProperties(activeType);
|
|
193
|
+
const discriminators = new Set((SchemaEx.getDiscriminatingProps(ast) ?? []).map((name) => name.toString()));
|
|
194
|
+
if (discriminators.size === 0) return activeProps;
|
|
195
|
+
const fullType = SchemaEx.getDiscriminatedType(ast, {});
|
|
196
|
+
const fullDiscriminatorProps = fullType ? getFormProperties(fullType) : activeProps;
|
|
197
|
+
return activeProps.map((prop) => discriminators.has(prop.name.toString()) ? fullDiscriminatorProps.find((candidate) => candidate.name === prop.name) ?? prop : prop);
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Get the property types of an AST and filter out properties that are not form inputs.
|
|
201
|
+
*
|
|
202
|
+
* The `FormInputAnnotation` is read from the raw property signature's AST (before
|
|
203
|
+
* `encodedBoundAST` is applied by `SchemaEx.getProperties`), because `encodedBoundAST` strips
|
|
204
|
+
* annotations from non-keyword inner types (e.g., `Schema.Struct`, `Schema.Array`).
|
|
205
|
+
* Without this, fields like `Routine.input` (a JsonSchema struct) annotated
|
|
206
|
+
* `FormInputAnnotation.set(false)` would still render in the form.
|
|
207
|
+
*
|
|
208
|
+
* For optional fields, `prop.type` is the union `T | undefined` — the annotation
|
|
209
|
+
* lives on the inner `T`. Unwrap before reading so that the conventional pattern
|
|
210
|
+
* `Schema.String.pipe(FormInputAnnotation.set(false), Schema.optional)` works.
|
|
211
|
+
*
|
|
212
|
+
* Container properties (nested structs, unions, arrays) keep their *raw* AST rather than
|
|
213
|
+
* the `encodedBoundAST` produced by `SchemaEx.getProperties`. `encodedBoundAST` strips annotations
|
|
214
|
+
* from non-keyword inner types, so a `FormInputAnnotation` nested beneath an encoded container
|
|
215
|
+
* (e.g. `RouteLeg.geometry`, reached via `Segment → details → routes → Route → legs`) would
|
|
216
|
+
* otherwise be lost by the time recursion reaches it. Leaf/transformation fields stay encoded
|
|
217
|
+
* (e.g. a `DateTime` still renders as its encoded string input).
|
|
218
|
+
*/
|
|
219
|
+
var getFormProperties = (ast) => {
|
|
220
|
+
const signatures = SchemaAST.getPropertySignatures(ast);
|
|
221
|
+
const rawByName = new Map(signatures.map((prop) => [prop.name, unwrapOptional(prop)]));
|
|
222
|
+
const hidden = new Set(signatures.filter((prop) => Annotation.FormInputAnnotation.getFromAst(unwrapOptional(prop)).pipe(Option.getOrElse(() => true)) === false).map((prop) => prop.name));
|
|
223
|
+
return SchemaEx.getProperties(ast).filter((prop) => !hidden.has(prop.name)).map((prop) => {
|
|
224
|
+
const raw = rawByName.get(prop.name);
|
|
225
|
+
return raw && (SchemaEx.isNestedType(raw) || SchemaEx.isArrayType(raw)) ? {
|
|
226
|
+
...prop,
|
|
227
|
+
type: raw
|
|
228
|
+
} : prop;
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/util/refs.ts
|
|
233
|
+
var getRefProps = (ast) => {
|
|
234
|
+
if (SchemaEx.isArrayType(ast)) {
|
|
235
|
+
const elementType = SchemaEx.getArrayElementType(ast);
|
|
236
|
+
if (elementType) {
|
|
237
|
+
if (Ref.isRefType(elementType)) return {
|
|
238
|
+
ast: elementType,
|
|
239
|
+
isArray: true,
|
|
240
|
+
typename: SchemaEx.findAnnotation(elementType, ReferenceAnnotationId$1)?.typename
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (Ref.isRefType(ast)) return {
|
|
245
|
+
ast,
|
|
246
|
+
isArray: false,
|
|
247
|
+
typename: SchemaEx.findAnnotation(ast, ReferenceAnnotationId$1)?.typename
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/components/Form/meta-tags.ts
|
|
252
|
+
/**
|
|
253
|
+
* Form-field key for the synthetic meta-tags array spliced into property/create forms.
|
|
254
|
+
*
|
|
255
|
+
* An object's tags live in `Obj.getMeta(obj).tags` (off-schema), so the form surfaces them as an
|
|
256
|
+
* extra field rather than reading the schema. The key is underscore-prefixed so it never collides
|
|
257
|
+
* with a `tags` field an object schema declares itself — e.g. a `TagIndex` record on a feed host,
|
|
258
|
+
* whose `tags` is `Record<tagId, objectId[]>`, not an array. Extending such a schema with a `tags`
|
|
259
|
+
* array threw "overlapping types at path: [\"tags\"]"; the disjoint `_tags` key avoids it. The
|
|
260
|
+
* human-facing label is restored via the `title` annotation on {@link withMetaTags}.
|
|
261
|
+
*/
|
|
262
|
+
var META_TAGS_KEY = "_tags";
|
|
263
|
+
/**
|
|
264
|
+
* Splices the synthetic meta-tags array (an object's `meta.tags`, surfaced as {@link Tag} refs) onto
|
|
265
|
+
* a schema for editing and drops `id`. Keyed by {@link META_TAGS_KEY} so it never collides with a
|
|
266
|
+
* `tags` field the schema declares itself.
|
|
267
|
+
*/
|
|
268
|
+
var withMetaTags = (schema) => omitId(Schema.Struct({ [META_TAGS_KEY]: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.annotations({ title: "Tags" }), Schema.optional) }).pipe(Schema.extend(schema)));
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/hooks/useAsyncFieldEffect.ts
|
|
271
|
+
/** Picks the declared dependency fields from the form values — the subset a dynamic field subscribes to. */
|
|
272
|
+
var pickValues = (values, deps) => {
|
|
273
|
+
const subset = {};
|
|
274
|
+
for (const dep of deps) subset[dep] = values?.[dep];
|
|
275
|
+
return subset;
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Runs a self-contained Effect (`R = never`) whenever `key` changes, debounced and abortable. Used by
|
|
279
|
+
* dynamic form fields to load select options, derive values, or validate against an external source from
|
|
280
|
+
* the current form values. The latest `factory` is always used, but a run is only (re)triggered when
|
|
281
|
+
* `key` changes — derive `key` from the inputs the factory actually reads. Re-running interrupts the
|
|
282
|
+
* prior run (cancelling any in-flight request).
|
|
283
|
+
*/
|
|
284
|
+
var useAsyncFieldEffect = (factory, key, { debounceMs = 400, enabled = true } = {}) => {
|
|
285
|
+
const factoryRef = useRef(factory);
|
|
286
|
+
factoryRef.current = factory;
|
|
287
|
+
const [state, setState] = useState({
|
|
288
|
+
loading: false,
|
|
289
|
+
error: void 0,
|
|
290
|
+
data: void 0
|
|
291
|
+
});
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!enabled) {
|
|
294
|
+
setState({
|
|
295
|
+
loading: false,
|
|
296
|
+
error: void 0,
|
|
297
|
+
data: void 0
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
let cancel;
|
|
302
|
+
const timer = setTimeout(() => {
|
|
303
|
+
setState((prev) => ({
|
|
304
|
+
...prev,
|
|
305
|
+
loading: true,
|
|
306
|
+
error: void 0
|
|
307
|
+
}));
|
|
308
|
+
cancel = Effect.runCallback(factoryRef.current(), { onExit: (exit) => {
|
|
309
|
+
Exit.match(exit, {
|
|
310
|
+
onSuccess: (data) => setState({
|
|
311
|
+
loading: false,
|
|
312
|
+
error: void 0,
|
|
313
|
+
data
|
|
314
|
+
}),
|
|
315
|
+
onFailure: (cause) => {
|
|
316
|
+
if (!Cause.isInterruptedOnly(cause)) setState((prev) => ({
|
|
317
|
+
loading: false,
|
|
318
|
+
error: Cause.squash(cause),
|
|
319
|
+
data: prev.data
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
} });
|
|
324
|
+
}, debounceMs);
|
|
325
|
+
return () => {
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
cancel?.();
|
|
328
|
+
};
|
|
329
|
+
}, [
|
|
330
|
+
key,
|
|
331
|
+
enabled,
|
|
332
|
+
debounceMs
|
|
333
|
+
]);
|
|
334
|
+
return state;
|
|
335
|
+
};
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/hooks/useFormContext.ts
|
|
338
|
+
var [FormContextProvider, useFormContext] = createContext("Form");
|
|
339
|
+
/**
|
|
340
|
+
* Get the current form values.
|
|
341
|
+
*/
|
|
342
|
+
var useFormValues = (componentName, path, defaultValue) => {
|
|
343
|
+
const stablePath = useMemo(() => path ?? [], [path ? path.join(".") : void 0]);
|
|
344
|
+
const jsonPath = SchemaEx.createJsonPath(stablePath);
|
|
345
|
+
const { form: { values, onValueChange } } = useFormContext(componentName);
|
|
346
|
+
const value = SchemaEx.getValue(values, jsonPath);
|
|
347
|
+
const lastAppliedPathRef = useRef(null);
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
|
|
350
|
+
lastAppliedPathRef.current = jsonPath;
|
|
351
|
+
onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
|
|
352
|
+
}
|
|
353
|
+
}, [
|
|
354
|
+
value,
|
|
355
|
+
defaultValue,
|
|
356
|
+
onValueChange,
|
|
357
|
+
stablePath,
|
|
358
|
+
jsonPath
|
|
359
|
+
]);
|
|
360
|
+
return value;
|
|
361
|
+
};
|
|
362
|
+
/**
|
|
363
|
+
* Get the state props for the given field.
|
|
364
|
+
*/
|
|
365
|
+
var useFormFieldState = (componentName, path = []) => {
|
|
366
|
+
const stablePath = useMemo(() => path, [Array.isArray(path) ? path.join(".") : path]);
|
|
367
|
+
const { form: { getStatus, getValue, onBlur, onValueChange } } = useFormContext(componentName);
|
|
368
|
+
return useMemo(() => ({
|
|
369
|
+
getStatus: () => getStatus(stablePath),
|
|
370
|
+
getValue: () => getValue(stablePath),
|
|
371
|
+
onBlur: () => onBlur(stablePath),
|
|
372
|
+
onValueChange: (ast, value) => onValueChange(stablePath, ast, value)
|
|
373
|
+
}), [
|
|
374
|
+
getStatus,
|
|
375
|
+
getValue,
|
|
376
|
+
onBlur,
|
|
377
|
+
onValueChange,
|
|
378
|
+
stablePath
|
|
379
|
+
]);
|
|
380
|
+
};
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/hooks/useFormHandler.ts
|
|
383
|
+
var __dxlog_file$6 = "/__w/dxos/dxos/packages/ui/react-ui-form/src/hooks/useFormHandler.ts";
|
|
384
|
+
/**
|
|
385
|
+
* Creates a hook for managing form state, including values, validation, and submission.
|
|
386
|
+
* Deeply integrated with `@dxos/schema` for schema-based validation.
|
|
387
|
+
*/
|
|
388
|
+
var useFormHandler = ({ schema, autoSave, values: valuesProp, defaultValues: defaultValuesProp, onValuesChanged, onValidate, onSave, onCancel, ...props }) => {
|
|
389
|
+
const [changed, setChanged] = useState({});
|
|
390
|
+
const [touched, setTouched] = useState({});
|
|
391
|
+
const [errors, setErrors] = useState({});
|
|
392
|
+
const [saving, setSaving] = useState(false);
|
|
393
|
+
const defaultValues = useDefaultValue(defaultValuesProp, () => ({}));
|
|
394
|
+
const source = valuesProp ?? defaultValues;
|
|
395
|
+
const [overrides, setOverrides] = useState({});
|
|
396
|
+
const values = applyOverrides(source, overrides);
|
|
397
|
+
const validate = useCallback((values) => {
|
|
398
|
+
if (!schema) return false;
|
|
399
|
+
let errors = validateSchema(schema, values) ?? [];
|
|
400
|
+
log("validate", {
|
|
401
|
+
values,
|
|
402
|
+
errors
|
|
403
|
+
}, {
|
|
404
|
+
"~LogMeta": "~LogMeta",
|
|
405
|
+
F: __dxlog_file$6,
|
|
406
|
+
L: 160,
|
|
407
|
+
S: void 0
|
|
408
|
+
});
|
|
409
|
+
if (errors.length === 0 && onValidate) errors = onValidate(values) ?? [];
|
|
410
|
+
setErrors(flatMap(errors));
|
|
411
|
+
return errors.length === 0;
|
|
412
|
+
}, [schema, onValidate]);
|
|
413
|
+
const [schemaChanged, setSchemaChanged] = useState(false);
|
|
414
|
+
useEffect(() => setSchemaChanged(true), [schema]);
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
if (schemaChanged) {
|
|
417
|
+
validate(values);
|
|
418
|
+
setSchemaChanged(false);
|
|
419
|
+
}
|
|
420
|
+
}, [
|
|
421
|
+
validate,
|
|
422
|
+
values,
|
|
423
|
+
schemaChanged
|
|
424
|
+
]);
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
const reconciled = Object.keys(overrides).filter((path) => valuesEqual(SchemaEx.getValue(source, path), overrides[path]));
|
|
427
|
+
if (reconciled.length === 0) return;
|
|
428
|
+
setOverrides((prev) => omitPaths(prev, reconciled));
|
|
429
|
+
setChanged((prev) => omitPaths(prev, reconciled));
|
|
430
|
+
setTouched((prev) => omitPaths(prev, reconciled));
|
|
431
|
+
});
|
|
432
|
+
const isValid = useMemo(() => {
|
|
433
|
+
return Object.keys(errors).length === 0;
|
|
434
|
+
}, [errors]);
|
|
435
|
+
/**
|
|
436
|
+
* NOTE: We can submit if there is no touched field that has an error.
|
|
437
|
+
* If there's a validation message visible in the form, submit should be disabled.
|
|
438
|
+
*/
|
|
439
|
+
const canSave = useMemo(() => !saving && !Object.entries(touched).some(([path, isTouched]) => isTouched && Object.keys(errors).some((errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`))), [
|
|
440
|
+
touched,
|
|
441
|
+
errors,
|
|
442
|
+
saving
|
|
443
|
+
]);
|
|
444
|
+
const handleSave = useCallback(async () => {
|
|
445
|
+
if (validate(values)) {
|
|
446
|
+
setSaving(true);
|
|
447
|
+
try {
|
|
448
|
+
await onSave?.(values, {
|
|
449
|
+
changed,
|
|
450
|
+
isValid
|
|
451
|
+
});
|
|
452
|
+
} finally {
|
|
453
|
+
setSaving(false);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}, [
|
|
457
|
+
values,
|
|
458
|
+
validate,
|
|
459
|
+
onSave,
|
|
460
|
+
changed,
|
|
461
|
+
isValid
|
|
462
|
+
]);
|
|
463
|
+
const handleCancel = useCallback(() => {
|
|
464
|
+
onCancel?.();
|
|
465
|
+
}, [onCancel]);
|
|
466
|
+
const getStatus = useCallback((path) => {
|
|
467
|
+
const jsonPath = Array.isArray(path) ? SchemaEx.createJsonPath(path) : path;
|
|
468
|
+
const [_, error] = Object.entries(errors).find(([errorPath]) => errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`)) ?? [];
|
|
469
|
+
if (!touched[jsonPath]) return {
|
|
470
|
+
status: void 0,
|
|
471
|
+
error: void 0
|
|
472
|
+
};
|
|
473
|
+
return {
|
|
474
|
+
status: error ? "error" : void 0,
|
|
475
|
+
error: error ? error ?? void 0 : void 0
|
|
476
|
+
};
|
|
477
|
+
}, [errors, touched]);
|
|
478
|
+
const getValue = useCallback((path) => {
|
|
479
|
+
const jsonPath = SchemaEx.createJsonPath(path);
|
|
480
|
+
if (Object.prototype.hasOwnProperty.call(overrides, jsonPath)) return overrides[jsonPath];
|
|
481
|
+
return SchemaEx.getValue(source, jsonPath);
|
|
482
|
+
}, [source, overrides]);
|
|
483
|
+
const onValueChange = useCallback((path, type, value) => {
|
|
484
|
+
log("onValueChange", {
|
|
485
|
+
path,
|
|
486
|
+
value
|
|
487
|
+
}, {
|
|
488
|
+
"~LogMeta": "~LogMeta",
|
|
489
|
+
F: __dxlog_file$6,
|
|
490
|
+
L: 280,
|
|
491
|
+
S: void 0
|
|
492
|
+
});
|
|
493
|
+
const jsonPath = SchemaEx.createJsonPath(path);
|
|
494
|
+
let parsedValue = value;
|
|
495
|
+
try {
|
|
496
|
+
if (type._tag === "NumberKeyword") parsedValue = parseFloat(value) || 0;
|
|
497
|
+
} catch (err) {
|
|
498
|
+
log.catch(err, void 0, {
|
|
499
|
+
"~LogMeta": "~LogMeta",
|
|
500
|
+
F: __dxlog_file$6,
|
|
501
|
+
L: 290,
|
|
502
|
+
S: void 0
|
|
503
|
+
});
|
|
504
|
+
parsedValue = void 0;
|
|
505
|
+
}
|
|
506
|
+
const newOverrides = {
|
|
507
|
+
...overrides,
|
|
508
|
+
[jsonPath]: parsedValue
|
|
509
|
+
};
|
|
510
|
+
setOverrides(newOverrides);
|
|
511
|
+
const newChanged = { [jsonPath]: true };
|
|
512
|
+
setChanged((prev) => ({
|
|
513
|
+
...prev,
|
|
514
|
+
...newChanged
|
|
515
|
+
}));
|
|
516
|
+
const newValues = applyOverrides(source, newOverrides);
|
|
517
|
+
const isValid = validate(newValues);
|
|
518
|
+
onValuesChanged?.(newValues, {
|
|
519
|
+
isValid,
|
|
520
|
+
changed: newChanged
|
|
521
|
+
});
|
|
522
|
+
}, [
|
|
523
|
+
source,
|
|
524
|
+
overrides,
|
|
525
|
+
validate,
|
|
526
|
+
onValuesChanged
|
|
527
|
+
]);
|
|
528
|
+
const onBlur = useCallback(async (path) => {
|
|
529
|
+
const jsonPath = SchemaEx.createJsonPath(path);
|
|
530
|
+
setTouched((touched) => ({
|
|
531
|
+
...touched,
|
|
532
|
+
[jsonPath]: true
|
|
533
|
+
}));
|
|
534
|
+
const isValid = validate(values);
|
|
535
|
+
if (Object.keys(changed).length > 0 && isValid && autoSave) await onSave?.(values, {
|
|
536
|
+
changed,
|
|
537
|
+
isValid
|
|
538
|
+
});
|
|
539
|
+
}, [
|
|
540
|
+
validate,
|
|
541
|
+
values,
|
|
542
|
+
changed,
|
|
543
|
+
autoSave,
|
|
544
|
+
onSave
|
|
545
|
+
]);
|
|
546
|
+
return useMemo(() => ({
|
|
547
|
+
schema,
|
|
548
|
+
values,
|
|
549
|
+
errors,
|
|
550
|
+
touched,
|
|
551
|
+
changed,
|
|
552
|
+
isValid,
|
|
553
|
+
canSave,
|
|
554
|
+
autoSave,
|
|
555
|
+
onSave: handleSave,
|
|
556
|
+
onCancel: handleCancel,
|
|
557
|
+
getStatus,
|
|
558
|
+
getValue,
|
|
559
|
+
onBlur,
|
|
560
|
+
onValueChange,
|
|
561
|
+
...props
|
|
562
|
+
}), [
|
|
563
|
+
schema,
|
|
564
|
+
autoSave,
|
|
565
|
+
values,
|
|
566
|
+
errors,
|
|
567
|
+
touched,
|
|
568
|
+
changed,
|
|
569
|
+
canSave,
|
|
570
|
+
isValid,
|
|
571
|
+
getStatus,
|
|
572
|
+
getValue,
|
|
573
|
+
onBlur,
|
|
574
|
+
onValueChange,
|
|
575
|
+
handleSave,
|
|
576
|
+
handleCancel
|
|
577
|
+
]);
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
580
|
+
* Creates a new object with value at path. Does not mutate source.
|
|
581
|
+
* Preserves arrays when path segments are numeric indices.
|
|
582
|
+
*/
|
|
583
|
+
var mergeAtPath = (obj, path, value) => {
|
|
584
|
+
if (path.length === 0) return value;
|
|
585
|
+
const [head, ...rest] = path;
|
|
586
|
+
const child = rest.length === 0 ? value : mergeAtPath(obj?.[head], rest, value);
|
|
587
|
+
if (Array.isArray(obj)) {
|
|
588
|
+
const copy = [...obj];
|
|
589
|
+
copy[head] = child;
|
|
590
|
+
return copy;
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
...obj,
|
|
594
|
+
[head]: child
|
|
595
|
+
};
|
|
596
|
+
};
|
|
597
|
+
/** Overlays sparse json-path edits onto a base value without mutating it; un-edited paths keep the base reference. */
|
|
598
|
+
var applyOverrides = (base, overrides) => {
|
|
599
|
+
let result = base;
|
|
600
|
+
for (const path of Object.keys(overrides)) result = mergeAtPath(result, SchemaEx.splitJsonPath(path), overrides[path]);
|
|
601
|
+
return result;
|
|
602
|
+
};
|
|
603
|
+
var valuesEqual = (left, right) => {
|
|
604
|
+
if (left === right) return true;
|
|
605
|
+
if (left === null || right === null) return left === right;
|
|
606
|
+
if (typeof left !== "object" || typeof right !== "object") return Utils.structuralRegion(() => Equal.equals(left, right));
|
|
607
|
+
if (Ref.isRef(left) && Ref.isRef(right)) return left.uri === right.uri;
|
|
608
|
+
if (Ref.isRef(left) || Ref.isRef(right)) return false;
|
|
609
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
610
|
+
if (left.length !== right.length) return false;
|
|
611
|
+
return left.every((value, index) => valuesEqual(value, right[index]));
|
|
612
|
+
}
|
|
613
|
+
if (Array.isArray(left) || Array.isArray(right)) return false;
|
|
614
|
+
const leftRecord = left;
|
|
615
|
+
const rightRecord = right;
|
|
616
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(leftRecord).filter((key) => key !== "id"), ...Object.keys(rightRecord).filter((key) => key !== "id")]);
|
|
617
|
+
for (const key of keys) if (!valuesEqual(leftRecord[key], rightRecord[key])) return false;
|
|
618
|
+
return true;
|
|
619
|
+
};
|
|
620
|
+
/** Returns a copy of `record` without the given json-path keys. */
|
|
621
|
+
var omitPaths = (record, paths) => {
|
|
622
|
+
const omit = new Set(paths);
|
|
623
|
+
return Object.fromEntries(Object.entries(record).filter(([key]) => !omit.has(key)));
|
|
624
|
+
};
|
|
625
|
+
var flatMap = (errors) => {
|
|
626
|
+
return errors.reduce((result, { path, message }) => {
|
|
627
|
+
const jsonPath = SchemaEx.fromEffectValidationPath(path);
|
|
628
|
+
if (!(jsonPath in result)) result[jsonPath] = message;
|
|
629
|
+
return result;
|
|
630
|
+
}, {});
|
|
631
|
+
};
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/hooks/useKeyHandler.tsx
|
|
634
|
+
/**
|
|
635
|
+
* Key handler.
|
|
636
|
+
*/
|
|
637
|
+
var useKeyHandler = (elRef, form) => {
|
|
638
|
+
const handleKeyDown = useCallback((event) => {
|
|
639
|
+
switch (event.key) {
|
|
640
|
+
case "Enter":
|
|
641
|
+
if (event.metaKey && form.canSave) form.onSave();
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
}, [
|
|
645
|
+
form.isValid,
|
|
646
|
+
form.canSave,
|
|
647
|
+
form.onSave,
|
|
648
|
+
form.autoSave
|
|
649
|
+
]);
|
|
650
|
+
useLayoutEffect(() => {
|
|
651
|
+
const el = elRef.current;
|
|
652
|
+
if (!el) return;
|
|
653
|
+
return addEventListener(el, "keydown", handleKeyDown);
|
|
654
|
+
}, [elRef, handleKeyDown]);
|
|
655
|
+
};
|
|
656
|
+
/** {@link Form} theme template: a tailwind-variants recipe (`styles`) plus non-class `behavior`. */
|
|
657
|
+
var formTheme = {
|
|
658
|
+
styles: tv({
|
|
659
|
+
slots: {
|
|
660
|
+
viewport: "",
|
|
661
|
+
content: "pb-form-padding",
|
|
662
|
+
section: "flex flex-col py-form-section-gap first:pt-0",
|
|
663
|
+
group: "flex flex-col gap-trim-md p-trim-md border border-separator rounded-sm",
|
|
664
|
+
sectionHeader: "",
|
|
665
|
+
sectionTitle: "text-lg",
|
|
666
|
+
sectionDescription: "text-description",
|
|
667
|
+
fieldSet: "",
|
|
668
|
+
field: "",
|
|
669
|
+
fieldLabel: "min-h-(--dx-control) grid grid-cols-[1fr_auto_auto_auto] items-center select-none",
|
|
670
|
+
fieldLabelText: "",
|
|
671
|
+
fieldDescription: "text-description",
|
|
672
|
+
fieldControl: "",
|
|
673
|
+
fieldValidation: "",
|
|
674
|
+
actions: "grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding",
|
|
675
|
+
submit: "flex w-full pt-form-padding",
|
|
676
|
+
fieldSetBody: "flex flex-col px-trim-sm pb-trim-sm",
|
|
677
|
+
fieldSetBox: "border border-subdued-separator rounded-sm",
|
|
678
|
+
fieldSetBoxOuter: ""
|
|
679
|
+
},
|
|
680
|
+
variants: { variant: {
|
|
681
|
+
default: {},
|
|
682
|
+
settings: {
|
|
683
|
+
content: "dx-document",
|
|
684
|
+
section: "py-form-section-gap! gap-trim-md",
|
|
685
|
+
sectionHeader: "pb-form-section-gap",
|
|
686
|
+
sectionTitle: "px-trim-md text-xl",
|
|
687
|
+
sectionDescription: "px-trim-md",
|
|
688
|
+
fieldSet: "flex flex-col gap-trim-md",
|
|
689
|
+
fieldSetBody: "flex flex-col gap-trim-md px-trim-sm pb-trim-sm",
|
|
690
|
+
field: mx("grid", "grid-cols-1 [grid-template-areas:\"header\"\"description\"\"control\"\"validation\"]", "md:grid-cols-2 md:[grid-template-areas:\"header_header\"\"description_control\"\"validation_validation\"]", "gap-x-trim-xl gap-y-0 p-trim-md border border-input-separator rounded-md"),
|
|
691
|
+
fieldLabel: "[grid-area:header]",
|
|
692
|
+
fieldLabelText: "text-base-fg text-lg",
|
|
693
|
+
fieldDescription: "[grid-area:description] pt-trim-xs text-description",
|
|
694
|
+
fieldControl: "[grid-area:control] flex justify-end items-start pt-trim-md md:pt-0",
|
|
695
|
+
fieldValidation: "[grid-area:validation]"
|
|
696
|
+
}
|
|
697
|
+
} },
|
|
698
|
+
defaultVariants: { variant: "default" }
|
|
699
|
+
}),
|
|
700
|
+
behavior: {
|
|
701
|
+
default: { showDescription: false },
|
|
702
|
+
settings: { showDescription: true }
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
Object.keys(formTheme.styles());
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/components/Form/FormField/fields/ArrayField/default-value.ts
|
|
708
|
+
/**
|
|
709
|
+
* Returns the default empty value for a given AST.
|
|
710
|
+
* Used for initializing new array values etc.
|
|
711
|
+
*/
|
|
712
|
+
var getDefaultValue = (ast) => {
|
|
713
|
+
switch (ast?._tag) {
|
|
714
|
+
case "StringKeyword": return "";
|
|
715
|
+
case "NumberKeyword": return 0;
|
|
716
|
+
case "BooleanKeyword": return false;
|
|
717
|
+
case "Suspend": return getDefaultValue(ast.f());
|
|
718
|
+
case "Refinement": {
|
|
719
|
+
const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(ast));
|
|
720
|
+
if (jsonSchema != null && "minimum" in jsonSchema && typeof jsonSchema.minimum === "number") return jsonSchema.minimum;
|
|
721
|
+
return getDefaultValue(ast.from);
|
|
722
|
+
}
|
|
723
|
+
default: if (ast && SchemaEx.isNestedType(ast)) return {};
|
|
724
|
+
else throw new Error(`Unsupported type: ${ast?._tag}`);
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/components/Form/FormField/FormField.tsx
|
|
729
|
+
var FormField = (props) => {
|
|
730
|
+
const { type, name, label: labelProp, path, required, projection, fieldMap, fieldProvider, readonly, hideEmpty = true, layout, createTypename, createOptionLabel, createOptionIcon, createInitialValuePath, createFieldMap, db, useType, getOptions, onCreate, resolveCreateEntry, refInline } = props;
|
|
731
|
+
const { t } = useTranslation(translationKey);
|
|
732
|
+
const title = SchemaEx.getAnnotation(SchemaAST.TitleAnnotationId)(type);
|
|
733
|
+
const description = SchemaEx.getAnnotation(SchemaAST.DescriptionAnnotationId)(type);
|
|
734
|
+
const examples = SchemaEx.getAnnotation(SchemaAST.ExamplesAnnotationId)(type);
|
|
735
|
+
const label = useMemo(() => labelProp ?? title ?? (name == null ? "" : String$1.capitalize(name)), [
|
|
736
|
+
labelProp,
|
|
737
|
+
title,
|
|
738
|
+
name
|
|
739
|
+
]);
|
|
740
|
+
const placeholder = useMemo(() => examples?.length ? `${t("example.placeholder")}: ${examples[0]}` : description ?? label, [
|
|
741
|
+
examples,
|
|
742
|
+
description,
|
|
743
|
+
label,
|
|
744
|
+
t
|
|
745
|
+
]);
|
|
746
|
+
const providerSchema = useMemo(() => fieldProvider ? Schema.make(type) : void 0, [fieldProvider, type]);
|
|
747
|
+
const fieldState = useFormFieldState(FormField.displayName, path);
|
|
748
|
+
const jsonPath = SchemaEx.createJsonPath(path ?? []);
|
|
749
|
+
const fieldProps = {
|
|
750
|
+
type,
|
|
751
|
+
format: Format.FormatAnnotation.getFromAst(type).pipe((annotation) => Option.getOrUndefined(annotation)),
|
|
752
|
+
readonly,
|
|
753
|
+
label,
|
|
754
|
+
description,
|
|
755
|
+
jsonPath,
|
|
756
|
+
placeholder,
|
|
757
|
+
presentation: layout,
|
|
758
|
+
required,
|
|
759
|
+
db,
|
|
760
|
+
...fieldState
|
|
761
|
+
};
|
|
762
|
+
if (readonly && hideEmpty && fieldState.getValue() == null) return null;
|
|
763
|
+
const CustomField = fieldMap?.[jsonPath];
|
|
764
|
+
if (CustomField) return /* @__PURE__ */ jsx(CustomField, { ...fieldProps });
|
|
765
|
+
if (fieldProvider && providerSchema) {
|
|
766
|
+
const component = fieldProvider({
|
|
767
|
+
schema: providerSchema,
|
|
768
|
+
prop: name ?? "",
|
|
769
|
+
fieldProps
|
|
770
|
+
});
|
|
771
|
+
if (component) return component;
|
|
772
|
+
}
|
|
773
|
+
const optionsLookup = Option.getOrUndefined(OptionsLookupAnnotation.getFromAst(type));
|
|
774
|
+
if (optionsLookup) return optionsLookup.combobox ? /* @__PURE__ */ jsx(ComboboxField, {
|
|
775
|
+
...fieldProps,
|
|
776
|
+
lookup: optionsLookup
|
|
777
|
+
}) : /* @__PURE__ */ jsx(AsyncSelectField, {
|
|
778
|
+
...fieldProps,
|
|
779
|
+
lookup: optionsLookup
|
|
780
|
+
});
|
|
781
|
+
const autofill = Option.getOrUndefined(AutofillAnnotation.getFromAst(type));
|
|
782
|
+
if (autofill) return /* @__PURE__ */ jsx(AutofillField, {
|
|
783
|
+
...fieldProps,
|
|
784
|
+
autofill
|
|
785
|
+
});
|
|
786
|
+
if (SchemaEx.isArrayType(type)) return /* @__PURE__ */ jsx(ArrayField, {
|
|
787
|
+
fieldProps: fieldState,
|
|
788
|
+
label,
|
|
789
|
+
...props
|
|
790
|
+
});
|
|
791
|
+
const Field = getFormField(fieldProps);
|
|
792
|
+
if (Field) return /* @__PURE__ */ jsx(Field, { ...fieldProps });
|
|
793
|
+
const options = getSelectOptions(type);
|
|
794
|
+
if (options) {
|
|
795
|
+
const selectOptions = ((projection?.getFieldProjections())?.find((fp) => fp.field.path === name))?.props.options;
|
|
796
|
+
return /* @__PURE__ */ jsx(SelectField, {
|
|
797
|
+
...fieldProps,
|
|
798
|
+
options: options.map((option) => {
|
|
799
|
+
return {
|
|
800
|
+
value: option,
|
|
801
|
+
label: (selectOptions?.find((so) => so.id === globalThis.String(option)))?.title ?? option.toString()
|
|
802
|
+
};
|
|
803
|
+
})
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
const refProps = getRefProps(type);
|
|
807
|
+
if (refProps) {
|
|
808
|
+
if ((refInline || Annotation.FormInlineAnnotation.getFromAst(refProps.ast).pipe(Option.getOrElse(() => false))) && !refProps.isArray) return /* @__PURE__ */ jsx(InlineRefField, {
|
|
809
|
+
...fieldProps,
|
|
810
|
+
...refProps,
|
|
811
|
+
db,
|
|
812
|
+
useType,
|
|
813
|
+
onCreate,
|
|
814
|
+
resolveCreateEntry
|
|
815
|
+
});
|
|
816
|
+
const isCreateTarget = !createTypename || refProps.typename === createTypename;
|
|
817
|
+
return /* @__PURE__ */ jsx(RefField, {
|
|
818
|
+
...fieldProps,
|
|
819
|
+
...refProps,
|
|
820
|
+
createOptionLabel: isCreateTarget ? createOptionLabel : void 0,
|
|
821
|
+
createOptionIcon: isCreateTarget ? createOptionIcon : void 0,
|
|
822
|
+
createInitialValuePath: isCreateTarget ? createInitialValuePath : void 0,
|
|
823
|
+
createFieldMap: isCreateTarget ? createFieldMap : void 0,
|
|
824
|
+
db,
|
|
825
|
+
useType,
|
|
826
|
+
getOptions,
|
|
827
|
+
onCreate,
|
|
828
|
+
resolveCreateEntry
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
if (SchemaEx.isNestedType(type)) {
|
|
832
|
+
const baseNode = SchemaEx.findNode(type, SchemaEx.isDiscriminatedUnion);
|
|
833
|
+
const typeLiteral = baseNode ? SchemaEx.getDiscriminatedType(baseNode, fieldState.getValue()) : SchemaEx.findNode(type, SchemaAST.isTypeLiteral);
|
|
834
|
+
if (typeLiteral) return /* @__PURE__ */ jsx(FormFieldSet, {
|
|
835
|
+
schema: Schema.make(typeLiteral),
|
|
836
|
+
path,
|
|
837
|
+
readonly,
|
|
838
|
+
layout,
|
|
839
|
+
label,
|
|
840
|
+
collapsible: true,
|
|
841
|
+
projection,
|
|
842
|
+
fieldMap,
|
|
843
|
+
fieldProvider,
|
|
844
|
+
createOptionLabel,
|
|
845
|
+
createOptionIcon,
|
|
846
|
+
createInitialValuePath,
|
|
847
|
+
db,
|
|
848
|
+
useType,
|
|
849
|
+
getOptions,
|
|
850
|
+
onCreate,
|
|
851
|
+
resolveCreateEntry
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
};
|
|
856
|
+
FormField.displayName = "Form.FormField";
|
|
857
|
+
var CompactIconButton = ({ classNames, ...props }) => {
|
|
858
|
+
return /* @__PURE__ */ jsx("span", {
|
|
859
|
+
className: "grid size-8 shrink-0 place-items-center",
|
|
860
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
861
|
+
variant: "ghost",
|
|
862
|
+
iconOnly: true,
|
|
863
|
+
...props,
|
|
864
|
+
classNames: mx("min-h-0 h-7 w-7 p-0", classNames)
|
|
865
|
+
})
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
/**
|
|
869
|
+
* Get property input component.
|
|
870
|
+
*/
|
|
871
|
+
var getFormField = ({ type, format }) => {
|
|
872
|
+
if (SchemaAST.isRefinement(type)) return getFormField({
|
|
873
|
+
type: type.from,
|
|
874
|
+
format
|
|
875
|
+
});
|
|
876
|
+
const formatField = Match.value(format).pipe(Match.withReturnType(), Match.when(Format.TypeFormat.Date, () => DateField), Match.when(Format.TypeFormat.DateTime, () => DateField), Match.when(Format.TypeFormat.GeoPoint, () => GeoPointField), Match.when(Format.TypeFormat.Markdown, () => MarkdownField), Match.when(Format.TypeFormat.Password, () => PasswordField), Match.when(Format.TypeFormat.Text, () => TextAreaField), Match.when(Format.TypeFormat.Time, () => DateField), Match.orElse(() => void 0));
|
|
877
|
+
if (formatField) return formatField;
|
|
878
|
+
switch (type._tag) {
|
|
879
|
+
case "AnyKeyword":
|
|
880
|
+
case "StringKeyword": return TextField;
|
|
881
|
+
case "NumberKeyword": return NumberField;
|
|
882
|
+
case "BooleanKeyword": return BooleanField;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
var getSelectOptions = (ast) => {
|
|
886
|
+
if (SchemaEx.isLiteralUnion(ast)) return ast.types.map((type) => type.literal).filter((v) => v !== null);
|
|
887
|
+
return Format.OptionsAnnotation.getFromAst(ast).pipe((annotation) => Option.getOrUndefined(annotation));
|
|
888
|
+
};
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/components/Form/FormField/presentation.tsx
|
|
891
|
+
/**
|
|
892
|
+
* Derive the {@link FormFieldPresentation} for a layout.
|
|
893
|
+
* - `static` renders read-only plain DOM.
|
|
894
|
+
* - `inline` drops the label (the control stands alone, e.g. an array row).
|
|
895
|
+
* - `full` adds the inline error block.
|
|
896
|
+
*/
|
|
897
|
+
var presentationFor = (layout) => ({
|
|
898
|
+
layout,
|
|
899
|
+
isStatic: layout === "static",
|
|
900
|
+
showLabel: layout !== "inline",
|
|
901
|
+
showError: layout === "full"
|
|
902
|
+
});
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/components/Form/FormField/FormRow.tsx
|
|
905
|
+
var FormFieldLabel = ({ classNames, labelClassName, variant = "default", label, error, readonly, required, standalone, button, labelEnd, onClick }) => {
|
|
906
|
+
const styles = formTheme.styles({ variant });
|
|
907
|
+
const labelClassNames = mx("text-sm text-description", required && "after:content-['*'] after:ms-0.5 after:text-warning-text", styles.fieldLabelText({ class: labelClassName }));
|
|
908
|
+
const labelNode = readonly || standalone ? /* @__PURE__ */ jsx("span", {
|
|
909
|
+
className: labelClassNames,
|
|
910
|
+
children: label
|
|
911
|
+
}) : /* @__PURE__ */ jsx(Input.Label, {
|
|
912
|
+
classNames: labelClassNames,
|
|
913
|
+
children: label
|
|
914
|
+
});
|
|
915
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
916
|
+
className: styles.fieldLabel({ class: mx(onClick && "cursor-pointer", classNames) }),
|
|
917
|
+
onClick,
|
|
918
|
+
children: [
|
|
919
|
+
labelNode,
|
|
920
|
+
labelEnd,
|
|
921
|
+
error ? /* @__PURE__ */ jsx(Tooltip.Trigger, {
|
|
922
|
+
asChild: true,
|
|
923
|
+
content: error,
|
|
924
|
+
side: "bottom",
|
|
925
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
926
|
+
icon: "ph--warning--regular",
|
|
927
|
+
size: 4,
|
|
928
|
+
classNames: "text-error-text"
|
|
929
|
+
})
|
|
930
|
+
}) : /* @__PURE__ */ jsx("span", {}),
|
|
931
|
+
button
|
|
932
|
+
]
|
|
933
|
+
});
|
|
934
|
+
};
|
|
935
|
+
FormFieldLabel.displayName = "Form.FieldLabel";
|
|
936
|
+
/**
|
|
937
|
+
* Formats a value for `static` (read-only, plain-DOM) presentation based on its
|
|
938
|
+
* type `format`. Dates/times are rendered human-readable; everything else falls
|
|
939
|
+
* back to `String(value)`.
|
|
940
|
+
*/
|
|
941
|
+
var formatStaticValue = (value, format$1) => {
|
|
942
|
+
if (value == null) return "";
|
|
943
|
+
switch (format$1) {
|
|
944
|
+
case Format.TypeFormat.DateTime:
|
|
945
|
+
case Format.TypeFormat.Date:
|
|
946
|
+
case Format.TypeFormat.Time: {
|
|
947
|
+
const date = new Date(value);
|
|
948
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
949
|
+
return format(date, format$1 === Format.TypeFormat.DateTime ? "PPp" : format$1 === Format.TypeFormat.Date ? "PP" : "p");
|
|
950
|
+
}
|
|
951
|
+
default: return String(value);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
var FORM_ROW_NAME = "Form.Row";
|
|
955
|
+
/**
|
|
956
|
+
* A labeled card row — the single shell behind both schema fields and free-form "action" rows. Field
|
|
957
|
+
* renderers pass a render-prop `children` (field mode: bound to the form value, with validation and
|
|
958
|
+
* static rendering); consumers pass plain `children` for an arbitrary control (action mode), the
|
|
959
|
+
* affordance for an arbitrary labeled control (action rows, settings cards).
|
|
960
|
+
*/
|
|
961
|
+
var FormRow = ({ classNames, children, readonly, presentation, label, description, jsonPath, format, required, standalone, labelEnd, renderStatic, validation, getStatus, getValue, rootRef }) => {
|
|
962
|
+
const { variant = "default" } = useFormContext(FORM_ROW_NAME);
|
|
963
|
+
const styles = formTheme.styles({ variant });
|
|
964
|
+
const { showDescription } = formTheme.behavior[variant];
|
|
965
|
+
const resolved = presentationFor(presentation);
|
|
966
|
+
if (typeof children === "function" && getStatus && getValue) {
|
|
967
|
+
const { status, error } = getStatus();
|
|
968
|
+
const value = getValue();
|
|
969
|
+
if (resolved.isStatic && value == null) return null;
|
|
970
|
+
const control = resolved.isStatic ? renderStatic?.(value) ?? /* @__PURE__ */ jsx("p", {
|
|
971
|
+
className: "truncate min-w-0",
|
|
972
|
+
children: formatStaticValue(value, format)
|
|
973
|
+
}) : children({
|
|
974
|
+
value,
|
|
975
|
+
presentation: resolved
|
|
976
|
+
});
|
|
977
|
+
return /* @__PURE__ */ jsx(Input.Root, {
|
|
978
|
+
validationValence: status,
|
|
979
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
980
|
+
className: styles.field({ class: classNames }),
|
|
981
|
+
ref: rootRef,
|
|
982
|
+
children: [
|
|
983
|
+
resolved.showLabel && /* @__PURE__ */ jsx(FormFieldLabel, {
|
|
984
|
+
variant,
|
|
985
|
+
error,
|
|
986
|
+
readonly,
|
|
987
|
+
required,
|
|
988
|
+
standalone,
|
|
989
|
+
labelEnd,
|
|
990
|
+
label,
|
|
991
|
+
path: jsonPath
|
|
992
|
+
}),
|
|
993
|
+
showDescription && description && /* @__PURE__ */ jsx(Input.Description, {
|
|
994
|
+
classNames: styles.fieldDescription(),
|
|
995
|
+
children: description
|
|
996
|
+
}),
|
|
997
|
+
/* @__PURE__ */ jsx("div", {
|
|
998
|
+
className: styles.fieldControl(),
|
|
999
|
+
children: control
|
|
1000
|
+
}),
|
|
1001
|
+
resolved.showError && error && /* @__PURE__ */ jsx("div", {
|
|
1002
|
+
className: styles.fieldValidation(),
|
|
1003
|
+
children: /* @__PURE__ */ jsx(Input.DescriptionAndValidation, { children: /* @__PURE__ */ jsx(Input.Validation, { children: error }) })
|
|
1004
|
+
})
|
|
1005
|
+
]
|
|
1006
|
+
})
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1010
|
+
ref: rootRef,
|
|
1011
|
+
className: styles.field({ class: classNames }),
|
|
1012
|
+
children: [
|
|
1013
|
+
resolved.showLabel && label && /* @__PURE__ */ jsx(FormFieldLabel, {
|
|
1014
|
+
variant,
|
|
1015
|
+
readonly,
|
|
1016
|
+
required,
|
|
1017
|
+
standalone: true,
|
|
1018
|
+
labelEnd,
|
|
1019
|
+
label,
|
|
1020
|
+
path: jsonPath
|
|
1021
|
+
}),
|
|
1022
|
+
showDescription && description && /* @__PURE__ */ jsx("p", {
|
|
1023
|
+
className: styles.fieldDescription(),
|
|
1024
|
+
children: description
|
|
1025
|
+
}),
|
|
1026
|
+
/* @__PURE__ */ jsx("div", {
|
|
1027
|
+
className: styles.fieldControl(),
|
|
1028
|
+
children: typeof children === "function" ? null : children
|
|
1029
|
+
}),
|
|
1030
|
+
validation && /* @__PURE__ */ jsx("div", {
|
|
1031
|
+
className: styles.fieldValidation(),
|
|
1032
|
+
children: validation
|
|
1033
|
+
})
|
|
1034
|
+
]
|
|
1035
|
+
});
|
|
1036
|
+
};
|
|
1037
|
+
FormRow.displayName = FORM_ROW_NAME;
|
|
1038
|
+
var FormFieldErrorBoundary = class extends Component {
|
|
1039
|
+
static getDerivedStateFromError(error) {
|
|
1040
|
+
return { error };
|
|
1041
|
+
}
|
|
1042
|
+
state = { error: void 0 };
|
|
1043
|
+
componentDidUpdate(prevProps) {
|
|
1044
|
+
if (prevProps.path !== this.props.path) this.resetError();
|
|
1045
|
+
}
|
|
1046
|
+
render() {
|
|
1047
|
+
if (this.state.error) return /* @__PURE__ */ jsxs("div", {
|
|
1048
|
+
className: "flex gap-2 border border-error-border font-mono text-sm",
|
|
1049
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1050
|
+
className: "bg-error-bg text-base-fg px-1 font-thin",
|
|
1051
|
+
children: "ERROR"
|
|
1052
|
+
}), String(this.props.path?.join("."))]
|
|
1053
|
+
});
|
|
1054
|
+
return this.props.children;
|
|
1055
|
+
}
|
|
1056
|
+
resetError() {
|
|
1057
|
+
this.setState({ error: void 0 });
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
//#endregion
|
|
1061
|
+
//#region src/components/Form/FormField/FormFieldHeader.tsx
|
|
1062
|
+
/**
|
|
1063
|
+
* Header row for a labelled group or list: a label with optional trailing controls (an inline add
|
|
1064
|
+
* affordance and/or arbitrary `actions`). Shared by array/list fields ({@link ArrayField}, the view
|
|
1065
|
+
* editor) and {@link FormFieldSetContainer}'s collapse header, so all group/list headers render identically.
|
|
1066
|
+
*/
|
|
1067
|
+
var FormFieldHeader = ({ label, path, required, readonly, classNames, add, actions, onClick }) => /* @__PURE__ */ jsx(FormFieldLabel, {
|
|
1068
|
+
standalone: true,
|
|
1069
|
+
classNames,
|
|
1070
|
+
label,
|
|
1071
|
+
required,
|
|
1072
|
+
readonly,
|
|
1073
|
+
path,
|
|
1074
|
+
button: !readonly && add || actions ? /* @__PURE__ */ jsxs(Fragment$1, { children: [!readonly && add && /* @__PURE__ */ jsx(CompactIconButton, {
|
|
1075
|
+
disabled: add.disabled,
|
|
1076
|
+
icon: add.icon ?? "ph--plus--regular",
|
|
1077
|
+
label: add.label,
|
|
1078
|
+
onClick: (event) => {
|
|
1079
|
+
event.stopPropagation();
|
|
1080
|
+
add.onClick();
|
|
1081
|
+
}
|
|
1082
|
+
}), actions] }) : void 0,
|
|
1083
|
+
onClick
|
|
1084
|
+
});
|
|
1085
|
+
//#endregion
|
|
1086
|
+
//#region src/components/Form/FormField/fields/ArrayField/ArrayField.tsx
|
|
1087
|
+
var __dxlog_file$5 = "/__w/dxos/dxos/packages/ui/react-ui-form/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx";
|
|
1088
|
+
var DND_ID = "__dndId";
|
|
1089
|
+
var isOrderedItem = (item) => !!item && typeof item === "object" && typeof item[DND_ID] === "string";
|
|
1090
|
+
var getOrderedId = (item) => item[DND_ID];
|
|
1091
|
+
var ArrayField = ({ type, path, label, readonly, layout, fieldProps: inputProps, ...props }) => {
|
|
1092
|
+
const { t } = useTranslation(translationKey);
|
|
1093
|
+
const elementType = SchemaEx.getArrayElementType(type);
|
|
1094
|
+
const { onValueChange } = inputProps;
|
|
1095
|
+
const values = useFormValues(ArrayField.displayName, path, () => []);
|
|
1096
|
+
const ordered = Annotation.FormOrderedAnnotation.getFromAst(type).pipe(Option.getOrElse(() => false));
|
|
1097
|
+
const { db, onCreate, getCreateDefaults, useType: useType$4 = useType } = props;
|
|
1098
|
+
const createTypename = Annotation.FormCreateAnnotation.getFromAst(type).pipe(Option.getOrUndefined);
|
|
1099
|
+
const createSchema = useType$4(db, createTypename ? DXN$1.make(createTypename) : void 0);
|
|
1100
|
+
const createInline = !!createTypename && !!elementType && Ref.isRefType(elementType) && !readonly && layout !== "static";
|
|
1101
|
+
const idsRef = useRef([]);
|
|
1102
|
+
const counterRef = useRef(0);
|
|
1103
|
+
const nextId = useCallback(() => `${DND_ID}-${counterRef.current++}`, []);
|
|
1104
|
+
const handleAddCreate = useCallback(async () => {
|
|
1105
|
+
if (!createSchema || !onCreate) return;
|
|
1106
|
+
const defaults = getCreateDefaults?.({
|
|
1107
|
+
jsonPath: SchemaEx.createJsonPath(path ?? []),
|
|
1108
|
+
schema: createSchema
|
|
1109
|
+
}) ?? {};
|
|
1110
|
+
const created = await onCreate(createSchema, defaults);
|
|
1111
|
+
if (!created) return;
|
|
1112
|
+
idsRef.current.push(nextId());
|
|
1113
|
+
onValueChange(type, [...values ?? [], Ref.make(created)]);
|
|
1114
|
+
}, [
|
|
1115
|
+
createSchema,
|
|
1116
|
+
onCreate,
|
|
1117
|
+
getCreateDefaults,
|
|
1118
|
+
onValueChange,
|
|
1119
|
+
type,
|
|
1120
|
+
path,
|
|
1121
|
+
values,
|
|
1122
|
+
nextId
|
|
1123
|
+
]);
|
|
1124
|
+
const getDefaultObjectValue = (typeNode) => {
|
|
1125
|
+
const baseNode = SchemaEx.findNode(typeNode, SchemaEx.isDiscriminatedUnion);
|
|
1126
|
+
const typeLiteral = baseNode ? SchemaEx.getDiscriminatedType(baseNode, {}) : SchemaEx.findNode(typeNode, SchemaAST.isTypeLiteral);
|
|
1127
|
+
if (!typeLiteral) return {};
|
|
1128
|
+
return Object.fromEntries(getFormProperties(typeLiteral).map((prop) => {
|
|
1129
|
+
const defaultValue = SchemaAST.getDefaultAnnotation(prop.type).pipe((annotation) => Option.getOrUndefined(annotation));
|
|
1130
|
+
return [prop.name, defaultValue];
|
|
1131
|
+
}));
|
|
1132
|
+
};
|
|
1133
|
+
const handleAdd = useCallback(() => {
|
|
1134
|
+
let defaultValue;
|
|
1135
|
+
try {
|
|
1136
|
+
defaultValue = elementType && SchemaEx.isNestedType(elementType) ? getDefaultObjectValue(elementType) : getDefaultValue(elementType);
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
log.catch(err, void 0, {
|
|
1139
|
+
"~LogMeta": "~LogMeta",
|
|
1140
|
+
F: __dxlog_file$5,
|
|
1141
|
+
L: 126,
|
|
1142
|
+
S: void 0
|
|
1143
|
+
});
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
idsRef.current.push(nextId());
|
|
1147
|
+
onValueChange(type, [...values ?? [], defaultValue]);
|
|
1148
|
+
}, [
|
|
1149
|
+
onValueChange,
|
|
1150
|
+
type,
|
|
1151
|
+
elementType,
|
|
1152
|
+
values,
|
|
1153
|
+
nextId
|
|
1154
|
+
]);
|
|
1155
|
+
const handleDelete = useCallback((idx) => {
|
|
1156
|
+
idsRef.current.splice(idx, 1);
|
|
1157
|
+
onValueChange(type, values?.filter((_, i) => i !== idx));
|
|
1158
|
+
}, [
|
|
1159
|
+
onValueChange,
|
|
1160
|
+
type,
|
|
1161
|
+
values
|
|
1162
|
+
]);
|
|
1163
|
+
const handleMove = useCallback((fromIndex, toIndex) => {
|
|
1164
|
+
const next = [...values ?? []];
|
|
1165
|
+
arrayMove(next, fromIndex, toIndex);
|
|
1166
|
+
arrayMove(idsRef.current, fromIndex, toIndex);
|
|
1167
|
+
onValueChange(type, next);
|
|
1168
|
+
}, [
|
|
1169
|
+
onValueChange,
|
|
1170
|
+
type,
|
|
1171
|
+
values
|
|
1172
|
+
]);
|
|
1173
|
+
if (!elementType || (readonly || layout === "static") && values && values.length < 1) return null;
|
|
1174
|
+
const renderItemAsObject = elementType && SchemaEx.isNestedType(elementType) && !Ref.isRefType(elementType);
|
|
1175
|
+
const renderField = (index, isLast) => {
|
|
1176
|
+
const field = /* @__PURE__ */ jsx(FormField, {
|
|
1177
|
+
...props,
|
|
1178
|
+
autoFocus: isLast,
|
|
1179
|
+
type: elementType,
|
|
1180
|
+
...renderItemAsObject || createInline ? { name: null } : { label },
|
|
1181
|
+
path: [...path ?? [], index],
|
|
1182
|
+
readonly: readonly || layout === "static",
|
|
1183
|
+
layout: renderItemAsObject ? layout === "static" ? "static" : void 0 : "inline",
|
|
1184
|
+
refInline: createInline || void 0
|
|
1185
|
+
});
|
|
1186
|
+
return createInline ? /* @__PURE__ */ jsx("div", {
|
|
1187
|
+
className: "w-full min-w-0 [--dx-col:auto]",
|
|
1188
|
+
children: field
|
|
1189
|
+
}) : field;
|
|
1190
|
+
};
|
|
1191
|
+
const header = (layout !== "static" || values && values.length > 0) && /* @__PURE__ */ jsx(FormFieldHeader, {
|
|
1192
|
+
label,
|
|
1193
|
+
path: SchemaEx.createJsonPath(path ?? []),
|
|
1194
|
+
readonly,
|
|
1195
|
+
add: layout !== "static" ? {
|
|
1196
|
+
label: t("add-item.button"),
|
|
1197
|
+
onClick: createInline ? () => void handleAddCreate() : handleAdd
|
|
1198
|
+
} : void 0
|
|
1199
|
+
});
|
|
1200
|
+
if (ordered && !readonly && layout !== "static") {
|
|
1201
|
+
const ids = idsRef.current;
|
|
1202
|
+
const count = values?.length ?? 0;
|
|
1203
|
+
while (ids.length < count) ids.push(nextId());
|
|
1204
|
+
if (ids.length > count) ids.length = count;
|
|
1205
|
+
const items = (values ?? []).map((value, index) => ({
|
|
1206
|
+
value,
|
|
1207
|
+
index,
|
|
1208
|
+
[DND_ID]: ids[index]
|
|
1209
|
+
}));
|
|
1210
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [header, /* @__PURE__ */ jsx(OrderedList.Root, {
|
|
1211
|
+
items,
|
|
1212
|
+
isItem: isOrderedItem,
|
|
1213
|
+
getId: getOrderedId,
|
|
1214
|
+
onMove: handleMove,
|
|
1215
|
+
children: ({ items }) => /* @__PURE__ */ jsx(OrderedList.Content, { children: items.map((item) => /* @__PURE__ */ jsxs(OrderedList.Item, {
|
|
1216
|
+
id: getOrderedId(item),
|
|
1217
|
+
item,
|
|
1218
|
+
classNames: "grid grid-cols-[min-content_1fr_min-content] items-start gap-1 p-0 pb-1",
|
|
1219
|
+
children: [
|
|
1220
|
+
/* @__PURE__ */ jsx(OrderedList.DragHandle, {}),
|
|
1221
|
+
renderField(item.index, item.index === items.length - 1),
|
|
1222
|
+
/* @__PURE__ */ jsx(CompactIconButton, {
|
|
1223
|
+
icon: "ph--x--regular",
|
|
1224
|
+
label: t("remove-item.button"),
|
|
1225
|
+
onClick: () => handleDelete(item.index)
|
|
1226
|
+
})
|
|
1227
|
+
]
|
|
1228
|
+
}, getOrderedId(item))) })
|
|
1229
|
+
})] });
|
|
1230
|
+
}
|
|
1231
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [header, /* @__PURE__ */ jsx("div", {
|
|
1232
|
+
className: "flex flex-col gap-2 pb-2",
|
|
1233
|
+
children: values?.map((_, index) => {
|
|
1234
|
+
const isLast = index === values.length - 1;
|
|
1235
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1236
|
+
className: mx("grid grid-cols-[minmax(0,1fr)_min-content] gap-1", renderItemAsObject || createInline ? "items-start" : "items-center"),
|
|
1237
|
+
children: [renderField(index, isLast), !readonly && layout !== "static" && /* @__PURE__ */ jsx(CompactIconButton, {
|
|
1238
|
+
icon: "ph--x--regular",
|
|
1239
|
+
label: t("remove-item.button"),
|
|
1240
|
+
onClick: () => handleDelete(index)
|
|
1241
|
+
})]
|
|
1242
|
+
}, index);
|
|
1243
|
+
})
|
|
1244
|
+
})] });
|
|
1245
|
+
};
|
|
1246
|
+
ArrayField.displayName = "Form.ArrayField";
|
|
1247
|
+
//#endregion
|
|
1248
|
+
//#region src/components/Form/FormField/fields/SelectField/create-select-field.tsx
|
|
1249
|
+
var __dxlog_file$4 = "/__w/dxos/dxos/packages/ui/react-ui-form/src/components/Form/FormField/fields/SelectField/create-select-field.tsx";
|
|
1250
|
+
/**
|
|
1251
|
+
* Factory for a `fieldMap` entry (keyed by JSON path) that renders a Select over a fixed list of
|
|
1252
|
+
* options, with an optional sentinel mapping to `undefined`. Use for fields whose choices come from a
|
|
1253
|
+
* runtime list rather than the schema (e.g. available model ids).
|
|
1254
|
+
*/
|
|
1255
|
+
var createSelectField = ({ options, defaultLabel = "Default" }) => {
|
|
1256
|
+
const normalized = options.map((option) => typeof option === "string" ? {
|
|
1257
|
+
value: option,
|
|
1258
|
+
label: option
|
|
1259
|
+
} : option);
|
|
1260
|
+
const hasDefault = defaultLabel !== null;
|
|
1261
|
+
const sentinel = "__default__";
|
|
1262
|
+
invariant(!normalized.some((option) => option.value === sentinel), `createSelectField: option value '${sentinel}' is reserved.`, {
|
|
1263
|
+
"~LogMeta": "~LogMeta",
|
|
1264
|
+
F: __dxlog_file$4,
|
|
1265
|
+
L: 39,
|
|
1266
|
+
S: void 0,
|
|
1267
|
+
A: ["!normalized.some((option) => option.value === sentinel)", "`createSelectField: option value '${sentinel}' is reserved.`"]
|
|
1268
|
+
});
|
|
1269
|
+
return ({ type, readonly, onValueChange, ...props }) => /* @__PURE__ */ jsx(FormRow, {
|
|
1270
|
+
readonly,
|
|
1271
|
+
renderStatic: (value) => /* @__PURE__ */ jsx("p", {
|
|
1272
|
+
className: "truncate min-w-0",
|
|
1273
|
+
children: normalized.find((option) => option.value === value)?.label ?? String(value ?? "")
|
|
1274
|
+
}),
|
|
1275
|
+
...props,
|
|
1276
|
+
children: ({ value }) => /* @__PURE__ */ jsxs(Select.Root, {
|
|
1277
|
+
disabled: !!readonly,
|
|
1278
|
+
value: value ?? sentinel,
|
|
1279
|
+
onValueChange: (next) => onValueChange(type, hasDefault && next === sentinel ? void 0 : next),
|
|
1280
|
+
children: [/* @__PURE__ */ jsx(Select.TriggerButton, {
|
|
1281
|
+
classNames: "w-full",
|
|
1282
|
+
disabled: !!readonly
|
|
1283
|
+
}), normalized.length > 0 && /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsxs(Select.Content, { children: [/* @__PURE__ */ jsxs(Select.Viewport, { children: [hasDefault && /* @__PURE__ */ jsx(Select.Option, {
|
|
1284
|
+
value: sentinel,
|
|
1285
|
+
children: defaultLabel
|
|
1286
|
+
}), normalized.map((option) => /* @__PURE__ */ jsx(Select.Option, {
|
|
1287
|
+
value: option.value,
|
|
1288
|
+
children: option.label ?? option.value
|
|
1289
|
+
}, option.value))] }), /* @__PURE__ */ jsx(Select.Arrow, {})] }) })]
|
|
1290
|
+
})
|
|
1291
|
+
});
|
|
1292
|
+
};
|
|
1293
|
+
//#endregion
|
|
1294
|
+
//#region src/components/Form/FormField/fields/SelectField/SelectField.tsx
|
|
1295
|
+
var SelectField = ({ type, readonly, placeholder, options, onValueChange, ...props }) => {
|
|
1296
|
+
const handleValueChange = useCallback((value) => onValueChange(type, value), [type, onValueChange]);
|
|
1297
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1298
|
+
readonly,
|
|
1299
|
+
renderStatic: (value) => /* @__PURE__ */ jsx("p", {
|
|
1300
|
+
className: "truncate min-w-0",
|
|
1301
|
+
children: options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)
|
|
1302
|
+
}),
|
|
1303
|
+
...props,
|
|
1304
|
+
children: ({ value }) => /* @__PURE__ */ jsxs(Select.Root, {
|
|
1305
|
+
value,
|
|
1306
|
+
onValueChange: handleValueChange,
|
|
1307
|
+
disabled: !!readonly,
|
|
1308
|
+
children: [/* @__PURE__ */ jsx(Select.TriggerButton, {
|
|
1309
|
+
classNames: "w-full",
|
|
1310
|
+
disabled: !!readonly,
|
|
1311
|
+
placeholder
|
|
1312
|
+
}), options?.length !== 0 && /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsxs(Select.Content, { children: [/* @__PURE__ */ jsx(Select.Viewport, { children: options?.map(({ value, label, secondaryLabel, icon, iconHue }) => /* @__PURE__ */ jsx(Select.Option, {
|
|
1313
|
+
value: String(value),
|
|
1314
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
1315
|
+
className: "flex items-center flex-row gap-2",
|
|
1316
|
+
children: [
|
|
1317
|
+
icon && /* @__PURE__ */ jsx(Icon, {
|
|
1318
|
+
icon,
|
|
1319
|
+
classNames: getIconHueStyles(iconHue)
|
|
1320
|
+
}),
|
|
1321
|
+
label ?? String(value),
|
|
1322
|
+
secondaryLabel && /* @__PURE__ */ jsx("span", {
|
|
1323
|
+
className: "text-subdued text-xs",
|
|
1324
|
+
children: secondaryLabel
|
|
1325
|
+
})
|
|
1326
|
+
]
|
|
1327
|
+
})
|
|
1328
|
+
}, String(value))) }), /* @__PURE__ */ jsx(Select.Arrow, {})] }) })]
|
|
1329
|
+
})
|
|
1330
|
+
});
|
|
1331
|
+
};
|
|
1332
|
+
var getIconHueStyles = (iconHue) => {
|
|
1333
|
+
return (iconHue ? getStyles(iconHue) : void 0)?.fg;
|
|
1334
|
+
};
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx
|
|
1337
|
+
/**
|
|
1338
|
+
* A select whose options are loaded dynamically (debounced) from the fields the lookup declares in its
|
|
1339
|
+
* `deps`, via an {@link Annotation.OptionsLookup}. Re-runs only when one of those values changes.
|
|
1340
|
+
* Auto-selects the sole option when exactly one resolves and nothing is chosen yet.
|
|
1341
|
+
*/
|
|
1342
|
+
var AsyncSelectField = ({ lookup, ...fieldProps }) => {
|
|
1343
|
+
const values = useFormValues(AsyncSelectField.displayName);
|
|
1344
|
+
const subset = useMemo(() => pickValues(values, lookup.deps), [values, lookup.deps]);
|
|
1345
|
+
const { loading, data } = useAsyncFieldEffect(() => lookup.load(subset), useMemo(() => JSON.stringify(subset), [subset]));
|
|
1346
|
+
const options = data ?? [];
|
|
1347
|
+
const { type, placeholder } = fieldProps;
|
|
1348
|
+
const current = fieldProps.getValue();
|
|
1349
|
+
const onValueChangeRef = useRef(fieldProps.onValueChange);
|
|
1350
|
+
onValueChangeRef.current = fieldProps.onValueChange;
|
|
1351
|
+
const autoSelectedDataRef = useRef(void 0);
|
|
1352
|
+
useEffect(() => {
|
|
1353
|
+
if ((current == null || current === "") && data?.length === 1 && data !== autoSelectedDataRef.current) {
|
|
1354
|
+
autoSelectedDataRef.current = data;
|
|
1355
|
+
onValueChangeRef.current(type, data[0].value);
|
|
1356
|
+
}
|
|
1357
|
+
}, [
|
|
1358
|
+
current,
|
|
1359
|
+
data,
|
|
1360
|
+
type
|
|
1361
|
+
]);
|
|
1362
|
+
const resolvedPlaceholder = loading ? "Loading…" : options.length === 0 ? "No options available" : placeholder;
|
|
1363
|
+
return /* @__PURE__ */ jsx(SelectField, {
|
|
1364
|
+
...fieldProps,
|
|
1365
|
+
placeholder: resolvedPlaceholder,
|
|
1366
|
+
options: options.map(({ value, label, secondaryLabel, icon }) => ({
|
|
1367
|
+
value,
|
|
1368
|
+
label,
|
|
1369
|
+
secondaryLabel,
|
|
1370
|
+
icon
|
|
1371
|
+
}))
|
|
1372
|
+
});
|
|
1373
|
+
};
|
|
1374
|
+
AsyncSelectField.displayName = "Form.AsyncSelectField";
|
|
1375
|
+
//#endregion
|
|
1376
|
+
//#region src/components/Form/FormField/fields/TextField/TextField.tsx
|
|
1377
|
+
var TextField = ({ type, readonly, placeholder, onBlur, onValueChange, ...props }) => {
|
|
1378
|
+
const handleChange = useCallback((event) => onValueChange(type, event.target.value), [type, onValueChange]);
|
|
1379
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1380
|
+
readonly,
|
|
1381
|
+
...props,
|
|
1382
|
+
children: ({ value = "" }) => /* @__PURE__ */ jsx(Input.TextInput, {
|
|
1383
|
+
noAutoFill: true,
|
|
1384
|
+
disabled: !!readonly,
|
|
1385
|
+
placeholder,
|
|
1386
|
+
value,
|
|
1387
|
+
onBlur,
|
|
1388
|
+
onChange: handleChange
|
|
1389
|
+
})
|
|
1390
|
+
});
|
|
1391
|
+
};
|
|
1392
|
+
//#endregion
|
|
1393
|
+
//#region src/components/Form/FormField/fields/AutofillField/AutofillField.tsx
|
|
1394
|
+
/**
|
|
1395
|
+
* An editable text field that pre-fills its value (debounced) from the fields the autofill declares in
|
|
1396
|
+
* its `deps`, via an {@link Autofill}. Re-runs only when one of those values changes. The
|
|
1397
|
+
* derived value is written only while the user has not typed their own (the field is empty or still
|
|
1398
|
+
* holds the last auto-filled value), so a manual edit is never clobbered.
|
|
1399
|
+
*/
|
|
1400
|
+
var AutofillField = ({ autofill, ...fieldProps }) => {
|
|
1401
|
+
const values = useFormValues(AutofillField.displayName);
|
|
1402
|
+
const subset = useMemo(() => pickValues(values, autofill.deps), [values, autofill.deps]);
|
|
1403
|
+
const { data } = useAsyncFieldEffect(() => autofill.derive(subset), useMemo(() => JSON.stringify(subset), [subset]));
|
|
1404
|
+
const { type } = fieldProps;
|
|
1405
|
+
const getValueRef = useRef(fieldProps.getValue);
|
|
1406
|
+
getValueRef.current = fieldProps.getValue;
|
|
1407
|
+
const onValueChangeRef = useRef(fieldProps.onValueChange);
|
|
1408
|
+
onValueChangeRef.current = fieldProps.onValueChange;
|
|
1409
|
+
const lastFilledRef = useRef(void 0);
|
|
1410
|
+
useEffect(() => {
|
|
1411
|
+
const current = getValueRef.current();
|
|
1412
|
+
if (typeof data !== "string" || data.length === 0) {
|
|
1413
|
+
if (lastFilledRef.current != null && current === lastFilledRef.current) {
|
|
1414
|
+
lastFilledRef.current = void 0;
|
|
1415
|
+
onValueChangeRef.current(type, "");
|
|
1416
|
+
}
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (current === data) {
|
|
1420
|
+
lastFilledRef.current = data;
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (current == null || current === "" || current === lastFilledRef.current) {
|
|
1424
|
+
lastFilledRef.current = data;
|
|
1425
|
+
onValueChangeRef.current(type, data);
|
|
1426
|
+
}
|
|
1427
|
+
}, [data, type]);
|
|
1428
|
+
return /* @__PURE__ */ jsx(TextField, { ...fieldProps });
|
|
1429
|
+
};
|
|
1430
|
+
AutofillField.displayName = "Form.AutofillField";
|
|
1431
|
+
//#endregion
|
|
1432
|
+
//#region src/components/Form/FormField/fields/BooleanField/BooleanField.tsx
|
|
1433
|
+
var BooleanField = ({ type, readonly, onValueChange, ...props }) => {
|
|
1434
|
+
const handleChange = useCallback((value) => onValueChange?.(type, value), [type, onValueChange]);
|
|
1435
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1436
|
+
readonly,
|
|
1437
|
+
...props,
|
|
1438
|
+
children: ({ value }) => /* @__PURE__ */ jsx(Input.Block, { children: /* @__PURE__ */ jsx(Input.Switch, {
|
|
1439
|
+
disabled: !!readonly,
|
|
1440
|
+
checked: value,
|
|
1441
|
+
onCheckedChange: handleChange
|
|
1442
|
+
}) })
|
|
1443
|
+
});
|
|
1444
|
+
};
|
|
1445
|
+
//#endregion
|
|
1446
|
+
//#region src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx
|
|
1447
|
+
/**
|
|
1448
|
+
* An editable combobox built on the shared {@link Combobox} primitive (trigger + popover search list,
|
|
1449
|
+
* the same family as `ObjectPicker`/`SearchList`). The trigger shows the current value; opening it reveals
|
|
1450
|
+
* a search input that loads suggestions (debounced) via an {@link OptionsLookup} and filters
|
|
1451
|
+
* them as you type. The literal typed text is offered as a fallback option at the bottom (deduped), so a
|
|
1452
|
+
* value not among the suggestions can still be selected. No options are shown until something is typed.
|
|
1453
|
+
*/
|
|
1454
|
+
var ComboboxField = ({ lookup, type, readonly, placeholder, onValueChange, ...props }) => {
|
|
1455
|
+
const values = useFormValues(ComboboxField.displayName);
|
|
1456
|
+
const ownKey = props.jsonPath;
|
|
1457
|
+
const [query, setQuery] = useState("");
|
|
1458
|
+
const [selectedLabel, setSelectedLabel] = useState();
|
|
1459
|
+
const lookupValues = useMemo(() => {
|
|
1460
|
+
const subset = pickValues(values, lookup.deps);
|
|
1461
|
+
if (ownKey && lookup.deps.includes(ownKey)) subset[ownKey] = query;
|
|
1462
|
+
return subset;
|
|
1463
|
+
}, [
|
|
1464
|
+
values,
|
|
1465
|
+
lookup.deps,
|
|
1466
|
+
ownKey,
|
|
1467
|
+
query
|
|
1468
|
+
]);
|
|
1469
|
+
const { data } = useAsyncFieldEffect(() => lookup.load(lookupValues), useMemo(() => JSON.stringify(lookupValues), [lookupValues]));
|
|
1470
|
+
const trimmed = query.trim();
|
|
1471
|
+
const normalized = trimmed.toLowerCase();
|
|
1472
|
+
const results = useMemo(() => normalized.length === 0 ? [] : (data ?? []).filter((option) => (option.label ?? option.value).toLowerCase().includes(normalized)), [data, normalized]);
|
|
1473
|
+
const hasExact = results.some((option) => option.value === trimmed);
|
|
1474
|
+
const handleValueChange = useCallback((next) => {
|
|
1475
|
+
setSelectedLabel((data ?? []).find((option) => option.value === next)?.label);
|
|
1476
|
+
onValueChange(type, next);
|
|
1477
|
+
}, [
|
|
1478
|
+
onValueChange,
|
|
1479
|
+
type,
|
|
1480
|
+
data
|
|
1481
|
+
]);
|
|
1482
|
+
const handleOpenChange = useCallback((open) => {
|
|
1483
|
+
if (!open) setQuery("");
|
|
1484
|
+
}, []);
|
|
1485
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1486
|
+
readonly,
|
|
1487
|
+
renderStatic: (value) => /* @__PURE__ */ jsx("p", {
|
|
1488
|
+
className: "truncate min-w-0",
|
|
1489
|
+
children: selectedLabel ?? value ?? ""
|
|
1490
|
+
}),
|
|
1491
|
+
...props,
|
|
1492
|
+
children: ({ value = "" }) => /* @__PURE__ */ jsxs(Combobox.Root, {
|
|
1493
|
+
value,
|
|
1494
|
+
displayValue: selectedLabel,
|
|
1495
|
+
onValueChange: handleValueChange,
|
|
1496
|
+
onOpenChange: handleOpenChange,
|
|
1497
|
+
placeholder,
|
|
1498
|
+
children: [/* @__PURE__ */ jsx(Combobox.Trigger, {
|
|
1499
|
+
disabled: !!readonly,
|
|
1500
|
+
classNames: "w-full"
|
|
1501
|
+
}), /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsxs(Combobox.Content, {
|
|
1502
|
+
resetSelectionOnChange: true,
|
|
1503
|
+
children: [
|
|
1504
|
+
/* @__PURE__ */ jsx(Combobox.Input, {
|
|
1505
|
+
autoFocus: true,
|
|
1506
|
+
value: query,
|
|
1507
|
+
onValueChange: setQuery,
|
|
1508
|
+
placeholder
|
|
1509
|
+
}),
|
|
1510
|
+
/* @__PURE__ */ jsxs(Combobox.List, { children: [results.map((option) => /* @__PURE__ */ jsx(Combobox.Item, {
|
|
1511
|
+
value: option.value,
|
|
1512
|
+
label: option.label ?? option.value,
|
|
1513
|
+
suffix: option.secondaryLabel
|
|
1514
|
+
}, option.value)), normalized.length > 0 && !hasExact && /* @__PURE__ */ jsx(Combobox.Item, {
|
|
1515
|
+
value: trimmed,
|
|
1516
|
+
label: trimmed
|
|
1517
|
+
})] }),
|
|
1518
|
+
/* @__PURE__ */ jsx(Combobox.Arrow, {})
|
|
1519
|
+
]
|
|
1520
|
+
}) })]
|
|
1521
|
+
})
|
|
1522
|
+
});
|
|
1523
|
+
};
|
|
1524
|
+
ComboboxField.displayName = "Form.ComboboxField";
|
|
1525
|
+
//#endregion
|
|
1526
|
+
//#region src/components/Form/FormField/fields/DateField/DateField.tsx
|
|
1527
|
+
/**
|
|
1528
|
+
* Stored value shapes:
|
|
1529
|
+
* - `Format.DateTime` -> ISO 8601 (`2018-11-13T20:20:39.000Z`).
|
|
1530
|
+
* - `Format.Date` -> `YYYY-MM-DD`.
|
|
1531
|
+
* - `Format.Time` -> `HH:mm:ss`.
|
|
1532
|
+
*
|
|
1533
|
+
* Segmented input value shapes (react-aria-components-backed):
|
|
1534
|
+
* - `Input.DateTime` -> `YYYY-MM-DDTHH:mm` in local time.
|
|
1535
|
+
* - `Input.Date` -> `YYYY-MM-DD`.
|
|
1536
|
+
* - `Input.Time` -> `HH:mm`.
|
|
1537
|
+
*/
|
|
1538
|
+
/** ISO 8601 → `YYYY-MM-DDTHH:mm` in the user's local timezone. */
|
|
1539
|
+
var isoToLocalDateTime = (value) => {
|
|
1540
|
+
if (!value) return "";
|
|
1541
|
+
const date = new Date(value);
|
|
1542
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
1543
|
+
return format(date, "yyyy-MM-dd'T'HH:mm");
|
|
1544
|
+
};
|
|
1545
|
+
/** `YYYY-MM-DDTHH:mm` (local) → ISO 8601 with timezone. */
|
|
1546
|
+
var localDateTimeToIso = (value) => {
|
|
1547
|
+
if (!value) return;
|
|
1548
|
+
const date = new Date(value);
|
|
1549
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
1550
|
+
};
|
|
1551
|
+
var DateField = ({ type, format, readonly, placeholder: _placeholder, onValueChange, onBlur: _onBlur, ...props }) => {
|
|
1552
|
+
const handleSimpleChange = useCallback((next) => onValueChange(type, next), [type, onValueChange]);
|
|
1553
|
+
const handleDateTimeChange = useCallback((next) => {
|
|
1554
|
+
onValueChange(type, localDateTimeToIso(next));
|
|
1555
|
+
}, [type, onValueChange]);
|
|
1556
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1557
|
+
...props,
|
|
1558
|
+
readonly,
|
|
1559
|
+
format,
|
|
1560
|
+
children: ({ value }) => {
|
|
1561
|
+
switch (format) {
|
|
1562
|
+
case Format.TypeFormat.Date: return /* @__PURE__ */ jsxs("div", {
|
|
1563
|
+
className: "grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch tabular-nums",
|
|
1564
|
+
children: [/* @__PURE__ */ jsx(Input.Date, {
|
|
1565
|
+
classNames: "min-w-0 overflow-hidden",
|
|
1566
|
+
disabled: readonly,
|
|
1567
|
+
value: value ?? "",
|
|
1568
|
+
onValueChange: handleSimpleChange
|
|
1569
|
+
}), /* @__PURE__ */ jsx(Input.TriggerIcon, {})]
|
|
1570
|
+
});
|
|
1571
|
+
case Format.TypeFormat.Time: return /* @__PURE__ */ jsx(Input.Time, {
|
|
1572
|
+
classNames: "tabular-nums",
|
|
1573
|
+
disabled: !!readonly,
|
|
1574
|
+
value: value ?? "",
|
|
1575
|
+
onValueChange: handleSimpleChange
|
|
1576
|
+
});
|
|
1577
|
+
case Format.TypeFormat.DateTime:
|
|
1578
|
+
default: return /* @__PURE__ */ jsxs("div", {
|
|
1579
|
+
className: "grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch tabular-nums",
|
|
1580
|
+
children: [/* @__PURE__ */ jsx(Input.DateTime, {
|
|
1581
|
+
classNames: "min-w-0 overflow-hidden",
|
|
1582
|
+
disabled: readonly,
|
|
1583
|
+
value: isoToLocalDateTime(value),
|
|
1584
|
+
onValueChange: handleDateTimeChange
|
|
1585
|
+
}), /* @__PURE__ */ jsx(Input.TriggerIcon, {})]
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
};
|
|
1591
|
+
//#endregion
|
|
1592
|
+
//#region src/components/Form/FormField/fields/GeoPointField/GeoPointField.tsx
|
|
1593
|
+
var GeoPointField = ({ type, readonly, getValue, onValueChange, onBlur, ...props }) => {
|
|
1594
|
+
const { t } = useTranslation(translationKey);
|
|
1595
|
+
const geoPoint = useMemo(() => getValue() ?? [0, 0], [getValue]);
|
|
1596
|
+
const value = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
|
|
1597
|
+
const [longitudeText, setLongitudeText] = useState(value.longitude?.toString());
|
|
1598
|
+
const [latitudeText, setLatitudeText] = useState(value.latitude?.toString());
|
|
1599
|
+
useEffect(() => {
|
|
1600
|
+
const location = GeoLocation.fromGeoPoint(geoPoint);
|
|
1601
|
+
setLongitudeText(location.longitude?.toString());
|
|
1602
|
+
setLatitudeText(location.latitude?.toString());
|
|
1603
|
+
}, [geoPoint]);
|
|
1604
|
+
const handleChange = useCallback((coordinateType, setText) => (event) => {
|
|
1605
|
+
const inputText = event.target.value;
|
|
1606
|
+
setText(inputText);
|
|
1607
|
+
if (inputText !== "" && inputText !== "-") {
|
|
1608
|
+
const coord = safeParseFloat(inputText);
|
|
1609
|
+
if (coord !== void 0 && !isNaN(coord)) {
|
|
1610
|
+
const newLocation = {
|
|
1611
|
+
...GeoLocation.fromGeoPoint(getValue() ?? [0, 0]),
|
|
1612
|
+
[coordinateType]: coord
|
|
1613
|
+
};
|
|
1614
|
+
onValueChange(type, GeoLocation.toGeoPoint(newLocation));
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}, [
|
|
1618
|
+
type,
|
|
1619
|
+
getValue,
|
|
1620
|
+
onValueChange
|
|
1621
|
+
]);
|
|
1622
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1623
|
+
readonly,
|
|
1624
|
+
getValue,
|
|
1625
|
+
standalone: true,
|
|
1626
|
+
renderStatic: (point) => {
|
|
1627
|
+
const location = GeoLocation.fromGeoPoint(point ?? [0, 0]);
|
|
1628
|
+
return !location.latitude && !location.longitude ? null : /* @__PURE__ */ jsx(LatLng, { ...location });
|
|
1629
|
+
},
|
|
1630
|
+
...props,
|
|
1631
|
+
children: ({ presentation }) => /* @__PURE__ */ jsxs("div", {
|
|
1632
|
+
className: "grid grid-cols-2 gap-form-gap",
|
|
1633
|
+
children: [/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(Input.Root, { children: [presentation.showLabel && /* @__PURE__ */ jsx(Input.Label, { children: t("latitude.label") }), /* @__PURE__ */ jsx(Input.TextInput, {
|
|
1634
|
+
type: "number",
|
|
1635
|
+
step: "0.00001",
|
|
1636
|
+
min: "-90",
|
|
1637
|
+
max: "90",
|
|
1638
|
+
disabled: !!readonly,
|
|
1639
|
+
placeholder: t("latitude.placeholder"),
|
|
1640
|
+
value: latitudeText ?? "",
|
|
1641
|
+
onChange: handleChange("latitude", setLatitudeText),
|
|
1642
|
+
onBlur
|
|
1643
|
+
})] }) }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(Input.Root, { children: [presentation.showLabel && /* @__PURE__ */ jsx(Input.Label, { children: t("longitude.label") }), /* @__PURE__ */ jsx(Input.TextInput, {
|
|
1644
|
+
type: "number",
|
|
1645
|
+
step: "0.00001",
|
|
1646
|
+
min: "-180",
|
|
1647
|
+
max: "180",
|
|
1648
|
+
disabled: !!readonly,
|
|
1649
|
+
placeholder: t("longitude.placeholder"),
|
|
1650
|
+
value: longitudeText ?? "",
|
|
1651
|
+
onChange: handleChange("longitude", setLongitudeText),
|
|
1652
|
+
onBlur
|
|
1653
|
+
})] }) })]
|
|
1654
|
+
})
|
|
1655
|
+
});
|
|
1656
|
+
};
|
|
1657
|
+
var LatLng = ({ latitude = 0, longitude = 0 }) => {
|
|
1658
|
+
const latHem = latitude >= 0 ? "N" : "S";
|
|
1659
|
+
const lngHem = longitude >= 0 ? "E" : "W";
|
|
1660
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
1661
|
+
className: "inline-flex items-center gap-form-gap",
|
|
1662
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", { children: Math.abs(latitude).toFixed(5) }), /* @__PURE__ */ jsxs("span", {
|
|
1663
|
+
className: "text-subdued",
|
|
1664
|
+
children: ["°", latHem]
|
|
1665
|
+
})] }), /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", { children: Math.abs(longitude).toFixed(5) }), /* @__PURE__ */ jsxs("span", {
|
|
1666
|
+
className: "text-subdued",
|
|
1667
|
+
children: ["°", lngHem]
|
|
1668
|
+
})] })]
|
|
1669
|
+
});
|
|
1670
|
+
};
|
|
1671
|
+
//#endregion
|
|
1672
|
+
//#region src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx
|
|
1673
|
+
/**
|
|
1674
|
+
* Form field that edits a markdown value in a CodeMirror editor.
|
|
1675
|
+
* Supports two backings:
|
|
1676
|
+
* - `Schema.String` annotated with `Format.TypeFormat.Markdown`: edited as plain text via `getValue`/`onValueChange`.
|
|
1677
|
+
* - `Ref<Text>` annotated with `Format.TypeFormat.Markdown`: edited in-place via an Automerge doc accessor.
|
|
1678
|
+
* When the ref is empty and a database is available, a create button is shown to allocate a new Text.
|
|
1679
|
+
*/
|
|
1680
|
+
var MarkdownField = ({ type, readonly, placeholder, db, getValue, onValueChange, ...props }) => {
|
|
1681
|
+
const isRef = Ref.isRefType(type);
|
|
1682
|
+
const renderStatic = (value) => {
|
|
1683
|
+
if (isRef) {
|
|
1684
|
+
const reference = value;
|
|
1685
|
+
return reference ? /* @__PURE__ */ jsx(RefStaticText, { reference }) : null;
|
|
1686
|
+
}
|
|
1687
|
+
return /* @__PURE__ */ jsx("p", {
|
|
1688
|
+
className: "whitespace-pre-wrap",
|
|
1689
|
+
children: value ?? ""
|
|
1690
|
+
});
|
|
1691
|
+
};
|
|
1692
|
+
const renderEditor = (value) => {
|
|
1693
|
+
if (isRef) {
|
|
1694
|
+
const reference = value;
|
|
1695
|
+
if (reference) return /* @__PURE__ */ jsx(RefMarkdownEditor, {
|
|
1696
|
+
reference,
|
|
1697
|
+
placeholder,
|
|
1698
|
+
readonly: !!readonly
|
|
1699
|
+
});
|
|
1700
|
+
if (readonly) return null;
|
|
1701
|
+
return /* @__PURE__ */ jsx(CreateTextButton, {
|
|
1702
|
+
db,
|
|
1703
|
+
disabled: !db,
|
|
1704
|
+
onCreate: (ref) => onValueChange(type, ref)
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
return /* @__PURE__ */ jsx(StringMarkdownEditor, {
|
|
1708
|
+
value: value ?? "",
|
|
1709
|
+
placeholder,
|
|
1710
|
+
readonly: !!readonly,
|
|
1711
|
+
onChange: (next) => onValueChange(type, next)
|
|
1712
|
+
});
|
|
1713
|
+
};
|
|
1714
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1715
|
+
readonly,
|
|
1716
|
+
getValue,
|
|
1717
|
+
renderStatic,
|
|
1718
|
+
...props,
|
|
1719
|
+
children: ({ value }) => renderEditor(value)
|
|
1720
|
+
});
|
|
1721
|
+
};
|
|
1722
|
+
/** Read-only static rendering for a `Ref<Text>` value: resolve the ref and print its content. */
|
|
1723
|
+
var RefStaticText = ({ reference }) => {
|
|
1724
|
+
const text = useAtomValue(useMemo(() => reference.atom, [reference]));
|
|
1725
|
+
if (!text?.content) return null;
|
|
1726
|
+
return /* @__PURE__ */ jsx("p", {
|
|
1727
|
+
className: "whitespace-pre-wrap",
|
|
1728
|
+
children: text.content
|
|
1729
|
+
});
|
|
1730
|
+
};
|
|
1731
|
+
var RefMarkdownEditor = ({ reference, placeholder, readonly }) => {
|
|
1732
|
+
const text = useAtomValue(useMemo(() => reference.atom, [reference]));
|
|
1733
|
+
const extensions = useBasicMarkdownExtensions({
|
|
1734
|
+
placeholder,
|
|
1735
|
+
readonly,
|
|
1736
|
+
extensions: useMemo(() => text ? [createDataExtensions({
|
|
1737
|
+
id: reference.uri,
|
|
1738
|
+
text: Doc.createAccessor(text, ["content"])
|
|
1739
|
+
})] : [], [text, reference])
|
|
1740
|
+
});
|
|
1741
|
+
if (!text) return null;
|
|
1742
|
+
return /* @__PURE__ */ jsx(Editor.Root, { children: /* @__PURE__ */ jsx(Editor.View, {
|
|
1743
|
+
classNames: "dx-input",
|
|
1744
|
+
extensions
|
|
1745
|
+
}) });
|
|
1746
|
+
};
|
|
1747
|
+
var StringMarkdownEditor = ({ value, placeholder, readonly, onChange }) => {
|
|
1748
|
+
const extensions = useBasicMarkdownExtensions({
|
|
1749
|
+
placeholder,
|
|
1750
|
+
readonly
|
|
1751
|
+
});
|
|
1752
|
+
const handleChange = useCallback((next) => onChange(next), [onChange]);
|
|
1753
|
+
return /* @__PURE__ */ jsx(Editor.Root, { children: /* @__PURE__ */ jsx(Editor.View, {
|
|
1754
|
+
classNames: "dx-input min-h-[6lh]",
|
|
1755
|
+
extensions,
|
|
1756
|
+
value,
|
|
1757
|
+
onChange: readonly ? void 0 : handleChange
|
|
1758
|
+
}) });
|
|
1759
|
+
};
|
|
1760
|
+
var CreateTextButton = ({ db, disabled, onCreate }) => {
|
|
1761
|
+
const { t } = useTranslation(translationKey);
|
|
1762
|
+
const handleClick = useCallback(() => {
|
|
1763
|
+
if (!db) return;
|
|
1764
|
+
onCreate(Ref.make(db.add(Text.make())));
|
|
1765
|
+
}, [db, onCreate]);
|
|
1766
|
+
return /* @__PURE__ */ jsx(IconButton, {
|
|
1767
|
+
icon: "ph--plus--regular",
|
|
1768
|
+
label: t("create-text.label"),
|
|
1769
|
+
onClick: handleClick,
|
|
1770
|
+
disabled
|
|
1771
|
+
});
|
|
1772
|
+
};
|
|
1773
|
+
//#endregion
|
|
1774
|
+
//#region src/components/Form/FormField/fields/NumberField/numeric-constraints.ts
|
|
1775
|
+
/**
|
|
1776
|
+
* Extracts numeric constraints (`minimum`/`maximum` bounds and whether the value must be an integer)
|
|
1777
|
+
* by walking the chain of Refinement ASTs and reading each one's JSON schema annotation (e.g. produced
|
|
1778
|
+
* by `Schema.between` / `Schema.int`). Returns empty constraints when none are declared.
|
|
1779
|
+
*/
|
|
1780
|
+
var getNumericConstraints = (ast) => {
|
|
1781
|
+
let node = ast;
|
|
1782
|
+
let min;
|
|
1783
|
+
let max;
|
|
1784
|
+
let integer = false;
|
|
1785
|
+
while (node && SchemaAST.isRefinement(node)) {
|
|
1786
|
+
const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(node));
|
|
1787
|
+
if (jsonSchema != null) {
|
|
1788
|
+
if ("minimum" in jsonSchema && typeof jsonSchema.minimum === "number") min = min === void 0 ? jsonSchema.minimum : Math.max(min, jsonSchema.minimum);
|
|
1789
|
+
if ("maximum" in jsonSchema && typeof jsonSchema.maximum === "number") max = max === void 0 ? jsonSchema.maximum : Math.min(max, jsonSchema.maximum);
|
|
1790
|
+
if ("type" in jsonSchema && jsonSchema.type === "integer" || "multipleOf" in jsonSchema && jsonSchema.multipleOf === 1) integer = true;
|
|
1791
|
+
}
|
|
1792
|
+
node = node.from;
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
min,
|
|
1796
|
+
max,
|
|
1797
|
+
integer
|
|
1798
|
+
};
|
|
1799
|
+
};
|
|
1800
|
+
//#endregion
|
|
1801
|
+
//#region src/components/Form/FormField/fields/NumberField/NumberField.tsx
|
|
1802
|
+
var NumberField = ({ type, readonly, placeholder, getValue, onValueChange, onBlur, ...props }) => {
|
|
1803
|
+
const { min, max, integer } = getNumericConstraints(type);
|
|
1804
|
+
const clamp = useCallback((n) => {
|
|
1805
|
+
let value = integer ? Math.round(n) : n;
|
|
1806
|
+
if (min !== void 0) value = Math.max(min, value);
|
|
1807
|
+
if (max !== void 0) value = Math.min(max, value);
|
|
1808
|
+
return value;
|
|
1809
|
+
}, [
|
|
1810
|
+
min,
|
|
1811
|
+
max,
|
|
1812
|
+
integer
|
|
1813
|
+
]);
|
|
1814
|
+
const [raw, setRaw] = useState(() => {
|
|
1815
|
+
const v = getValue();
|
|
1816
|
+
return v !== void 0 ? String(v) : "";
|
|
1817
|
+
});
|
|
1818
|
+
const externalValue = getValue();
|
|
1819
|
+
useEffect(() => {
|
|
1820
|
+
if (externalValue !== safeParseFloat(raw)) setRaw(externalValue !== void 0 ? String(externalValue) : "");
|
|
1821
|
+
}, [externalValue]);
|
|
1822
|
+
const handleChange = useCallback((event) => {
|
|
1823
|
+
const value = event.target.value;
|
|
1824
|
+
setRaw(value);
|
|
1825
|
+
const parsed = safeParseFloat(value);
|
|
1826
|
+
if (parsed !== void 0) onValueChange(type, clamp(parsed));
|
|
1827
|
+
}, [
|
|
1828
|
+
type,
|
|
1829
|
+
onValueChange,
|
|
1830
|
+
clamp
|
|
1831
|
+
]);
|
|
1832
|
+
const handleBlur = useCallback((event) => {
|
|
1833
|
+
const parsed = safeParseFloat(raw);
|
|
1834
|
+
if (parsed === void 0) {
|
|
1835
|
+
const committed = getValue();
|
|
1836
|
+
setRaw(committed !== void 0 ? String(committed) : "");
|
|
1837
|
+
} else setRaw(String(clamp(parsed)));
|
|
1838
|
+
onBlur(event);
|
|
1839
|
+
}, [
|
|
1840
|
+
raw,
|
|
1841
|
+
getValue,
|
|
1842
|
+
onBlur,
|
|
1843
|
+
clamp
|
|
1844
|
+
]);
|
|
1845
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1846
|
+
readonly,
|
|
1847
|
+
getValue,
|
|
1848
|
+
...props,
|
|
1849
|
+
children: () => /* @__PURE__ */ jsx(Input.TextInput, {
|
|
1850
|
+
type: "number",
|
|
1851
|
+
disabled: !!readonly,
|
|
1852
|
+
placeholder,
|
|
1853
|
+
value: raw,
|
|
1854
|
+
min,
|
|
1855
|
+
max,
|
|
1856
|
+
step: integer ? 1 : void 0,
|
|
1857
|
+
onChange: handleChange,
|
|
1858
|
+
onBlur: handleBlur
|
|
1859
|
+
})
|
|
1860
|
+
});
|
|
1861
|
+
};
|
|
1862
|
+
//#endregion
|
|
1863
|
+
//#region src/components/Form/FormField/fields/PasswordField/PasswordField.tsx
|
|
1864
|
+
var PasswordField = ({ type, readonly, placeholder, onBlur, onValueChange, ...props }) => {
|
|
1865
|
+
const handleChange = useCallback((event) => onValueChange(type, event.target.value), [type, onValueChange]);
|
|
1866
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
1867
|
+
readonly,
|
|
1868
|
+
...props,
|
|
1869
|
+
children: ({ value = "" }) => /* @__PURE__ */ jsx(Input.TextInput, {
|
|
1870
|
+
type: "password",
|
|
1871
|
+
noAutoFill: true,
|
|
1872
|
+
spellCheck: false,
|
|
1873
|
+
disabled: !!readonly,
|
|
1874
|
+
placeholder,
|
|
1875
|
+
value,
|
|
1876
|
+
onBlur,
|
|
1877
|
+
onChange: handleChange
|
|
1878
|
+
})
|
|
1879
|
+
});
|
|
1880
|
+
};
|
|
1881
|
+
//#endregion
|
|
1882
|
+
//#region src/components/Form/FormField/fields/RefField/find-ref-option.ts
|
|
1883
|
+
var isRefSnapshot = (val) => {
|
|
1884
|
+
return typeof val === "object" && typeof val?.["/"] === "string";
|
|
1885
|
+
};
|
|
1886
|
+
/**
|
|
1887
|
+
* Find the option a ref-like form value points at. Matches on the local (entity-id) form so a bare local EID
|
|
1888
|
+
* (`echo:/<id>`, produced by `Ref.make`) still resolves against an option keyed by the entity's qualified
|
|
1889
|
+
* self URI (`echo://<space>/<id>`). Returns `undefined` when the value is not a ref or no option matches.
|
|
1890
|
+
*
|
|
1891
|
+
* Comparing by entity id is only sound within one space (ids are unique there, not globally). Two EIDs that
|
|
1892
|
+
* both carry a space authority must therefore agree on it: a qualified value and a qualified option from
|
|
1893
|
+
* different spaces never match, even when their entity ids coincide.
|
|
1894
|
+
*/
|
|
1895
|
+
var findRefOption = (value, options) => {
|
|
1896
|
+
const isRef = Ref.isRef(value);
|
|
1897
|
+
if (!isRef && !isRefSnapshot(value)) return;
|
|
1898
|
+
const valueUri = isRef ? value.uri : value["/"];
|
|
1899
|
+
const directMatch = options.find((option) => option.id === valueUri);
|
|
1900
|
+
if (directMatch) return directMatch;
|
|
1901
|
+
const valueEid = EID$1.tryParse(valueUri);
|
|
1902
|
+
if (!valueEid) return;
|
|
1903
|
+
const valueSpaceId = EID$1.getSpaceId(valueEid);
|
|
1904
|
+
const valueLocal = EID$1.toLocal(valueEid);
|
|
1905
|
+
return options.find((option) => {
|
|
1906
|
+
const optionEid = EID$1.tryParse(option.id);
|
|
1907
|
+
if (!optionEid) return false;
|
|
1908
|
+
const optionSpaceId = EID$1.getSpaceId(optionEid);
|
|
1909
|
+
if (valueSpaceId != null && optionSpaceId != null && valueSpaceId !== optionSpaceId) return false;
|
|
1910
|
+
return EID$1.equals(EID$1.toLocal(optionEid), valueLocal);
|
|
1911
|
+
});
|
|
1912
|
+
};
|
|
1913
|
+
//#endregion
|
|
1914
|
+
//#region src/components/ObjectPicker/ObjectPicker.tsx
|
|
1915
|
+
var ObjectPickerContent = composable(({ options, selectedIds, createSchema, createOptionLabel, createOptionIcon, createInitialValuePath, createFieldMap, onSelect, onCreate, ...props }, forwardedRef) => {
|
|
1916
|
+
const { t } = useTranslation(translationKey);
|
|
1917
|
+
const [showForm, setShowForm] = useState(false);
|
|
1918
|
+
const [formInitialValue, setFormInitialValue] = useState("");
|
|
1919
|
+
const { results, query, handleSearch } = useSearchListResults({ items: options });
|
|
1920
|
+
const handleFormSave = useCallback(async (values) => {
|
|
1921
|
+
await onCreate?.(values);
|
|
1922
|
+
setShowForm(false);
|
|
1923
|
+
setFormInitialValue("");
|
|
1924
|
+
}, [onCreate]);
|
|
1925
|
+
const handleFormCancel = useCallback(() => {
|
|
1926
|
+
setShowForm(false);
|
|
1927
|
+
setFormInitialValue("");
|
|
1928
|
+
}, []);
|
|
1929
|
+
const handleKeyDown = useCallback((event) => {
|
|
1930
|
+
if (event.key === "Enter" && createSchema && event.currentTarget.querySelector("[role=\"option\"][aria-selected=\"true\"][data-value=\"__create__\"]")) {
|
|
1931
|
+
event.stopPropagation();
|
|
1932
|
+
event.preventDefault();
|
|
1933
|
+
const currentQuery = event.currentTarget.querySelector("input[type=\"text\"]")?.value || "";
|
|
1934
|
+
setFormInitialValue(currentQuery);
|
|
1935
|
+
setShowForm(true);
|
|
1936
|
+
}
|
|
1937
|
+
}, [createSchema]);
|
|
1938
|
+
if (showForm && createSchema) return /* @__PURE__ */ jsxs(Combobox.Content, {
|
|
1939
|
+
...props,
|
|
1940
|
+
onKeyDownCapture: handleKeyDown,
|
|
1941
|
+
ref: forwardedRef,
|
|
1942
|
+
children: [/* @__PURE__ */ jsx(Popover.Viewport, { children: /* @__PURE__ */ jsx(Form.Root, {
|
|
1943
|
+
testId: "create-referenced-object-form",
|
|
1944
|
+
schema: createSchema,
|
|
1945
|
+
defaultValues: createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {},
|
|
1946
|
+
fieldMap: createFieldMap,
|
|
1947
|
+
onSave: handleFormSave,
|
|
1948
|
+
onCancel: handleFormCancel,
|
|
1949
|
+
children: /* @__PURE__ */ jsx(Form.Viewport, { children: /* @__PURE__ */ jsxs(Form.Content, { children: [/* @__PURE__ */ jsx(Form.FieldSet, {}), /* @__PURE__ */ jsx(Form.Actions, {})] }) })
|
|
1950
|
+
}) }), /* @__PURE__ */ jsx(Combobox.Arrow, {})]
|
|
1951
|
+
});
|
|
1952
|
+
return /* @__PURE__ */ jsxs(Combobox.Content, {
|
|
1953
|
+
...props,
|
|
1954
|
+
onKeyDownCapture: handleKeyDown,
|
|
1955
|
+
ref: forwardedRef,
|
|
1956
|
+
children: [
|
|
1957
|
+
/* @__PURE__ */ jsx(Combobox.Input, {
|
|
1958
|
+
placeholder: t("ref-field-combobox-input.placeholder"),
|
|
1959
|
+
autoFocus: true,
|
|
1960
|
+
value: query,
|
|
1961
|
+
onValueChange: handleSearch
|
|
1962
|
+
}),
|
|
1963
|
+
/* @__PURE__ */ jsxs(Combobox.List, { children: [results.map((option) => /* @__PURE__ */ jsx(Combobox.Item, {
|
|
1964
|
+
value: option.id,
|
|
1965
|
+
label: option.label,
|
|
1966
|
+
description: option.description,
|
|
1967
|
+
checked: selectedIds?.includes(option.id),
|
|
1968
|
+
onSelect: () => onSelect?.(option.id),
|
|
1969
|
+
classNames: "flex items-center gap-2"
|
|
1970
|
+
}, option.id)), createSchema && onCreate && /* @__PURE__ */ jsx(CreateItem, {
|
|
1971
|
+
query,
|
|
1972
|
+
createOptionLabel,
|
|
1973
|
+
createOptionIcon: createOptionIcon ?? "ph--plus--regular",
|
|
1974
|
+
onCreateItemSelect: (currentQuery) => {
|
|
1975
|
+
setFormInitialValue(currentQuery);
|
|
1976
|
+
setShowForm(true);
|
|
1977
|
+
}
|
|
1978
|
+
})] }),
|
|
1979
|
+
/* @__PURE__ */ jsx(Combobox.Arrow, {})
|
|
1980
|
+
]
|
|
1981
|
+
});
|
|
1982
|
+
});
|
|
1983
|
+
var CreateItem = ({ query, createOptionLabel, createOptionIcon, onCreateItemSelect }) => {
|
|
1984
|
+
const { t } = useTranslation(translationKey);
|
|
1985
|
+
const label = createOptionLabel ? t(createOptionLabel[0], {
|
|
1986
|
+
ns: createOptionLabel[1].ns,
|
|
1987
|
+
text: query
|
|
1988
|
+
}) : t("create-option.label");
|
|
1989
|
+
return /* @__PURE__ */ jsx(Combobox.Item, {
|
|
1990
|
+
value: "__create__",
|
|
1991
|
+
label,
|
|
1992
|
+
icon: createOptionIcon,
|
|
1993
|
+
classNames: "flex items-center gap-2",
|
|
1994
|
+
closeOnSelect: false,
|
|
1995
|
+
onSelect: () => {
|
|
1996
|
+
onCreateItemSelect(query);
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
};
|
|
2000
|
+
ObjectPickerContent.displayName = "ObjectPicker.Content";
|
|
2001
|
+
var ObjectPicker = {
|
|
2002
|
+
Root: Combobox.Root,
|
|
2003
|
+
Portal: Combobox.Portal,
|
|
2004
|
+
Trigger: Combobox.Trigger,
|
|
2005
|
+
VirtualTrigger: Combobox.VirtualTrigger,
|
|
2006
|
+
Content: ObjectPickerContent
|
|
2007
|
+
};
|
|
2008
|
+
//#endregion
|
|
2009
|
+
//#region src/components/Form/FormField/fields/RefField/RefField.tsx
|
|
2010
|
+
var defaultGetOptions = (results, { parentLabel, getTypePlaceholder } = {}) => results.filter((result) => !parentLabel || Obj.getParent(result) != null).map((result) => {
|
|
2011
|
+
const id = Entity.getURI(result, { prefer: "named" });
|
|
2012
|
+
const labelEntity = (parentLabel ? Obj.getParent(result) : void 0) ?? result;
|
|
2013
|
+
const typename = Entity.getTypename(labelEntity);
|
|
2014
|
+
return {
|
|
2015
|
+
id,
|
|
2016
|
+
label: Entity.getLabel(labelEntity) ?? (typename ? getTypePlaceholder?.(typename) : void 0) ?? id
|
|
2017
|
+
};
|
|
2018
|
+
});
|
|
2019
|
+
var defaultUseResults = (db, typename) => useQuery(db, !typename ? Query.select(Filter.nothing()) : typename === ANY_OBJECT_TYPENAME ? Query.select(Filter.everything()) : Query.select(Filter.type(DXN$1.make(typename))).from(Scope.space(), Scope.registry()));
|
|
2020
|
+
var RefField = (props) => {
|
|
2021
|
+
const { type, readonly, label, jsonPath, placeholder, presentation, required, getStatus, getValue, createOptionLabel, createOptionIcon, createInitialValuePath, createFieldMap, db, useType: useType$3 = useType, useResults = defaultUseResults, getOptions = defaultGetOptions, onCreate, resolveCreateEntry, onValueChange } = props;
|
|
2022
|
+
const { t } = useTranslation(translationKey);
|
|
2023
|
+
const { status, error } = getStatus();
|
|
2024
|
+
const resolved = presentationFor(presentation);
|
|
2025
|
+
const typename = useMemo(() => type ? SchemaEx.findAnnotation(type, ReferenceAnnotationId)?.typename : void 0, [type]);
|
|
2026
|
+
const getTypePlaceholder = useCallback((placeholderTypename) => {
|
|
2027
|
+
const segment = placeholderTypename.split(/[.:/]/).filter(Boolean).pop() ?? placeholderTypename;
|
|
2028
|
+
const humanized = segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
2029
|
+
return t("object-name.placeholder", {
|
|
2030
|
+
ns: placeholderTypename,
|
|
2031
|
+
defaultValue: humanized
|
|
2032
|
+
});
|
|
2033
|
+
}, [t]);
|
|
2034
|
+
const results = useResults(db, typename);
|
|
2035
|
+
const options = useMemo(() => {
|
|
2036
|
+
const parentLabel = type ? SchemaEx.findAnnotation(type, ParentLabelAnnotationId) === true : false;
|
|
2037
|
+
return getOptions(results, {
|
|
2038
|
+
parentLabel,
|
|
2039
|
+
getTypePlaceholder
|
|
2040
|
+
});
|
|
2041
|
+
}, [
|
|
2042
|
+
results,
|
|
2043
|
+
getOptions,
|
|
2044
|
+
type,
|
|
2045
|
+
getTypePlaceholder
|
|
2046
|
+
]);
|
|
2047
|
+
const item = useCallback(() => findRefOption(getValue(), options), [options, getValue])();
|
|
2048
|
+
const selectedIds = useMemo(() => item ? [item.id] : [], [item]);
|
|
2049
|
+
const createSchema = useType$3(db, typename && typename !== ANY_OBJECT_TYPENAME ? DXN$1.make(typename) : void 0);
|
|
2050
|
+
const createEntry = typename ? resolveCreateEntry?.(typename) : void 0;
|
|
2051
|
+
const [open, setOpen] = useState(false);
|
|
2052
|
+
const handleCreate = useCallback(async (values) => {
|
|
2053
|
+
if (createEntry?.createObject && db) {
|
|
2054
|
+
const newObject = await createEntry.createObject(values, db);
|
|
2055
|
+
if (newObject) onValueChange(type, Ref.make(newObject));
|
|
2056
|
+
} else if (createSchema && onCreate) {
|
|
2057
|
+
const newObject = await onCreate(createSchema, values);
|
|
2058
|
+
if (newObject) onValueChange(type, Ref.make(newObject));
|
|
2059
|
+
}
|
|
2060
|
+
setOpen(false);
|
|
2061
|
+
}, [
|
|
2062
|
+
createEntry,
|
|
2063
|
+
db,
|
|
2064
|
+
createSchema,
|
|
2065
|
+
onCreate,
|
|
2066
|
+
type,
|
|
2067
|
+
onValueChange
|
|
2068
|
+
]);
|
|
2069
|
+
const handleUpdate = useCallback((id) => {
|
|
2070
|
+
const item = options.find((option) => option.id === id);
|
|
2071
|
+
const ref = item ? Ref.fromURI(URI.make(item.id)) : void 0;
|
|
2072
|
+
onValueChange(type, ref);
|
|
2073
|
+
}, [
|
|
2074
|
+
options,
|
|
2075
|
+
type,
|
|
2076
|
+
onValueChange
|
|
2077
|
+
]);
|
|
2078
|
+
const handleSelect = useCallback((id) => {
|
|
2079
|
+
if (item?.id === id) handleUpdate(void 0);
|
|
2080
|
+
else handleUpdate(id);
|
|
2081
|
+
}, [item, handleUpdate]);
|
|
2082
|
+
if (!typename || (readonly || resolved.isStatic) && !item) return null;
|
|
2083
|
+
return /* @__PURE__ */ jsxs(Input.Root, {
|
|
2084
|
+
validationValence: status,
|
|
2085
|
+
children: [
|
|
2086
|
+
resolved.showLabel && /* @__PURE__ */ jsx(FormFieldLabel, {
|
|
2087
|
+
error,
|
|
2088
|
+
readonly,
|
|
2089
|
+
required,
|
|
2090
|
+
label,
|
|
2091
|
+
path: jsonPath
|
|
2092
|
+
}),
|
|
2093
|
+
/* @__PURE__ */ jsx("div", { children: readonly ? !item ? /* @__PURE__ */ jsx("p", {
|
|
2094
|
+
className: "text-description mb-2",
|
|
2095
|
+
children: t("empty-readonly-ref-field.label")
|
|
2096
|
+
}) : /* @__PURE__ */ jsx(DxAnchor, {
|
|
2097
|
+
dxn: item.id,
|
|
2098
|
+
rootclassname: "me-1",
|
|
2099
|
+
children: item.label
|
|
2100
|
+
}, item.id) : /* @__PURE__ */ jsxs(ObjectPicker.Root, {
|
|
2101
|
+
open,
|
|
2102
|
+
onOpenChange: setOpen,
|
|
2103
|
+
children: [/* @__PURE__ */ jsx(ObjectPicker.Trigger, {
|
|
2104
|
+
asChild: true,
|
|
2105
|
+
classNames: "p-0",
|
|
2106
|
+
children: item ? /* @__PURE__ */ jsx("div", {
|
|
2107
|
+
className: "flex gap-form-gap w-full",
|
|
2108
|
+
children: /* @__PURE__ */ jsx(Input.Root, { children: /* @__PURE__ */ jsx(Input.TextInput, {
|
|
2109
|
+
value: item.label,
|
|
2110
|
+
readOnly: true,
|
|
2111
|
+
classNames: "w-full"
|
|
2112
|
+
}) }, item.id)
|
|
2113
|
+
}) : /* @__PURE__ */ jsxs(Button, {
|
|
2114
|
+
classNames: "w-full text-start gap-form-gap",
|
|
2115
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2116
|
+
className: "grow overflow-hidden",
|
|
2117
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
2118
|
+
className: "truncate text-description",
|
|
2119
|
+
children: placeholder || label || t("ref-field.placeholder")
|
|
2120
|
+
})
|
|
2121
|
+
}), /* @__PURE__ */ jsx(Icon, {
|
|
2122
|
+
size: 3,
|
|
2123
|
+
icon: "ph--caret-down--bold"
|
|
2124
|
+
})]
|
|
2125
|
+
})
|
|
2126
|
+
}), /* @__PURE__ */ jsx(ObjectPicker.Portal, { children: /* @__PURE__ */ jsx(ObjectPicker.Content, {
|
|
2127
|
+
classNames: "dx-card-popover-width",
|
|
2128
|
+
options,
|
|
2129
|
+
selectedIds,
|
|
2130
|
+
createSchema: createEntry?.inputSchema ?? (createSchema && omitHiddenFormFields(omitId(Type.getSchema(createSchema)))),
|
|
2131
|
+
createOptionLabel,
|
|
2132
|
+
createOptionIcon,
|
|
2133
|
+
createInitialValuePath,
|
|
2134
|
+
createFieldMap,
|
|
2135
|
+
onCreate: onCreate || createEntry?.createObject ? handleCreate : void 0,
|
|
2136
|
+
onSelect: handleSelect
|
|
2137
|
+
}) })]
|
|
2138
|
+
}) }),
|
|
2139
|
+
resolved.showError && /* @__PURE__ */ jsx(Input.DescriptionAndValidation, { children: error })
|
|
2140
|
+
]
|
|
2141
|
+
});
|
|
2142
|
+
};
|
|
2143
|
+
//#endregion
|
|
2144
|
+
//#region src/components/Form/FormField/fields/RefField/InlineRefField.tsx
|
|
2145
|
+
/**
|
|
2146
|
+
* Renders a referenced object's own fields inline (a nested form bound to the
|
|
2147
|
+
* target) rather than the {@link RefField} picker. Activated by
|
|
2148
|
+
* `FormInlineAnnotation.set(true)` on the `Ref` property.
|
|
2149
|
+
*
|
|
2150
|
+
* The nested form edits the target object directly: each changed path is
|
|
2151
|
+
* written back via `Obj.setValue` within an `Obj.update`, mirroring
|
|
2152
|
+
* `ObjectProperties` (without the synthetic meta-tags row).
|
|
2153
|
+
*/
|
|
2154
|
+
var InlineRefField = (props) => {
|
|
2155
|
+
const { type, readonly, label, jsonPath, presentation, required, db, getValue, onValueChange, onCreate, useType: useType$2 = useType } = props;
|
|
2156
|
+
const { t } = useTranslation(translationKey);
|
|
2157
|
+
const resolved = presentationFor(presentation);
|
|
2158
|
+
const reference = getValue();
|
|
2159
|
+
const typename = useMemo(() => type ? SchemaEx.findAnnotation(type, ReferenceAnnotationId$1)?.typename : void 0, [type]);
|
|
2160
|
+
const createType = useType$2(db, typename ? DXN$1.make(typename) : void 0);
|
|
2161
|
+
const handleCreate = useCallback(async () => {
|
|
2162
|
+
if (!createType || !onCreate) return;
|
|
2163
|
+
const newObject = await onCreate(createType, {});
|
|
2164
|
+
if (newObject) onValueChange(type, Ref.make(newObject));
|
|
2165
|
+
}, [
|
|
2166
|
+
createType,
|
|
2167
|
+
onCreate,
|
|
2168
|
+
type,
|
|
2169
|
+
onValueChange
|
|
2170
|
+
]);
|
|
2171
|
+
if (readonly && !reference) return null;
|
|
2172
|
+
return /* @__PURE__ */ jsxs(Input.Root, { children: [resolved.showLabel && /* @__PURE__ */ jsx(FormFieldLabel, {
|
|
2173
|
+
readonly,
|
|
2174
|
+
required,
|
|
2175
|
+
label,
|
|
2176
|
+
path: jsonPath
|
|
2177
|
+
}), reference ? /* @__PURE__ */ jsx(InlineForm, {
|
|
2178
|
+
reference,
|
|
2179
|
+
db,
|
|
2180
|
+
readonly,
|
|
2181
|
+
useType: useType$2
|
|
2182
|
+
}) : !readonly && onCreate && /* @__PURE__ */ jsx(IconButton, {
|
|
2183
|
+
classNames: "w-full gap-form-gap",
|
|
2184
|
+
disabled: !createType || !db,
|
|
2185
|
+
icon: "ph--plus--regular",
|
|
2186
|
+
label: label || t("ref-field.placeholder"),
|
|
2187
|
+
onClick: () => void handleCreate()
|
|
2188
|
+
})] });
|
|
2189
|
+
};
|
|
2190
|
+
var InlineForm = ({ reference, db, readonly, useType: useType$1 = useType }) => {
|
|
2191
|
+
const target = useAtomValue(useMemo(() => reference.atom, [reference]));
|
|
2192
|
+
const typeFromRegistry = useType$1(db, target ? Obj.getTypeURI(target) : void 0);
|
|
2193
|
+
const targetType = target && Obj.getType(target) || typeFromRegistry;
|
|
2194
|
+
const formSchema = useMemo(() => targetType ? omitId(Type.getSchema(targetType)) : void 0, [targetType]);
|
|
2195
|
+
const defaultValues = useMemo(() => target ? { ...target } : {}, [target]);
|
|
2196
|
+
const handleChange = useCallback((values, { isValid, changed }) => {
|
|
2197
|
+
if (!isValid || !target) return;
|
|
2198
|
+
const changedPaths = Object.keys(changed).filter((path) => changed[path]);
|
|
2199
|
+
if (changedPaths.length === 0) return;
|
|
2200
|
+
Obj.update(target, () => {
|
|
2201
|
+
for (const path of changedPaths) {
|
|
2202
|
+
const parts = SchemaEx.splitJsonPath(path);
|
|
2203
|
+
Obj.setValue(target, parts, Obj.getValue(values, parts));
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
}, [target]);
|
|
2207
|
+
if (!target || !formSchema) return null;
|
|
2208
|
+
return /* @__PURE__ */ jsx(FormRoot, {
|
|
2209
|
+
db,
|
|
2210
|
+
schema: formSchema,
|
|
2211
|
+
defaultValues,
|
|
2212
|
+
onValuesChanged: handleChange,
|
|
2213
|
+
children: /* @__PURE__ */ jsx(FormContent, { children: /* @__PURE__ */ jsx(FormFieldSetContainer$1, {
|
|
2214
|
+
collapsible: true,
|
|
2215
|
+
readonly
|
|
2216
|
+
}) })
|
|
2217
|
+
});
|
|
2218
|
+
};
|
|
2219
|
+
//#endregion
|
|
2220
|
+
//#region src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx
|
|
2221
|
+
var SelectOptionField = ({ type, readonly, label, jsonPath, getStatus, getValue, onValueChange }) => {
|
|
2222
|
+
const { t } = useTranslation(translationKey);
|
|
2223
|
+
const { status, error } = getStatus();
|
|
2224
|
+
const [selected, setSelectedId] = useState(null);
|
|
2225
|
+
const [isNewOption, setIsNewOption] = useState(false);
|
|
2226
|
+
const inputRef = useRef(null);
|
|
2227
|
+
const options = getValue();
|
|
2228
|
+
useEffect(() => {
|
|
2229
|
+
if (options === void 0) onValueChange(type, []);
|
|
2230
|
+
}, [
|
|
2231
|
+
options,
|
|
2232
|
+
type,
|
|
2233
|
+
onValueChange
|
|
2234
|
+
]);
|
|
2235
|
+
const randomHue = () => {
|
|
2236
|
+
const usedHues = new Set(options?.map((option) => option.color) ?? []);
|
|
2237
|
+
const unusedHues = hues.filter((hue) => !usedHues.has(hue));
|
|
2238
|
+
if (unusedHues.length > 0) return unusedHues[Math.floor(Math.random() * unusedHues.length)];
|
|
2239
|
+
const lastThree = new Set(options?.slice(-3).map((option) => option.color) ?? []);
|
|
2240
|
+
const availableHues = hues.filter((hue) => !lastThree.has(hue));
|
|
2241
|
+
return availableHues[Math.floor(Math.random() * availableHues.length)] ?? hues[Math.floor(Math.random() * hues.length)];
|
|
2242
|
+
};
|
|
2243
|
+
const handleAdd = useCallback(() => {
|
|
2244
|
+
const newId = PublicKey.random().truncate();
|
|
2245
|
+
const newOption = {
|
|
2246
|
+
id: newId,
|
|
2247
|
+
title: "",
|
|
2248
|
+
color: randomHue()
|
|
2249
|
+
};
|
|
2250
|
+
onValueChange(type, [...options ?? [], newOption]);
|
|
2251
|
+
setSelectedId(newId);
|
|
2252
|
+
setIsNewOption(true);
|
|
2253
|
+
}, [
|
|
2254
|
+
options,
|
|
2255
|
+
type,
|
|
2256
|
+
onValueChange
|
|
2257
|
+
]);
|
|
2258
|
+
const handleDelete = useCallback((id) => {
|
|
2259
|
+
onValueChange(type, options?.filter((option) => option.id !== id) ?? []);
|
|
2260
|
+
setSelectedId(null);
|
|
2261
|
+
}, [
|
|
2262
|
+
options,
|
|
2263
|
+
type,
|
|
2264
|
+
onValueChange
|
|
2265
|
+
]);
|
|
2266
|
+
const handleMove = useCallback((from, to) => {
|
|
2267
|
+
if (!options) return;
|
|
2268
|
+
const newOptions = [...options];
|
|
2269
|
+
const [removed] = newOptions.splice(from, 1);
|
|
2270
|
+
newOptions.splice(to, 0, removed);
|
|
2271
|
+
onValueChange(type, newOptions);
|
|
2272
|
+
}, [
|
|
2273
|
+
options,
|
|
2274
|
+
type,
|
|
2275
|
+
onValueChange
|
|
2276
|
+
]);
|
|
2277
|
+
const handleClick = useCallback((id) => {
|
|
2278
|
+
setSelectedId((current) => current === id ? null : id);
|
|
2279
|
+
}, []);
|
|
2280
|
+
const handleTitleChange = useCallback((id) => (e) => {
|
|
2281
|
+
const newOptions = options?.map((o) => o.id === id ? {
|
|
2282
|
+
...o,
|
|
2283
|
+
title: e.target.value
|
|
2284
|
+
} : o);
|
|
2285
|
+
onValueChange(type, newOptions ?? []);
|
|
2286
|
+
}, [
|
|
2287
|
+
options,
|
|
2288
|
+
type,
|
|
2289
|
+
onValueChange
|
|
2290
|
+
]);
|
|
2291
|
+
const handleKeyDown = useCallback((ev) => {
|
|
2292
|
+
if (ev.key === "Enter") setSelectedId(null);
|
|
2293
|
+
}, []);
|
|
2294
|
+
const handleColorChange = useCallback((id) => (hue) => {
|
|
2295
|
+
const newOptions = options?.map((o) => o.id === id ? {
|
|
2296
|
+
...o,
|
|
2297
|
+
color: hue
|
|
2298
|
+
} : o);
|
|
2299
|
+
onValueChange(type, newOptions ?? []);
|
|
2300
|
+
}, [
|
|
2301
|
+
options,
|
|
2302
|
+
type,
|
|
2303
|
+
onValueChange
|
|
2304
|
+
]);
|
|
2305
|
+
useEffect(() => {
|
|
2306
|
+
if (selected && isNewOption && inputRef.current) {
|
|
2307
|
+
inputRef.current.focus();
|
|
2308
|
+
setIsNewOption(false);
|
|
2309
|
+
}
|
|
2310
|
+
}, [selected, isNewOption]);
|
|
2311
|
+
return /* @__PURE__ */ jsxs(Input.Root, {
|
|
2312
|
+
validationValence: status,
|
|
2313
|
+
children: [/* @__PURE__ */ jsx(FormFieldLabel, {
|
|
2314
|
+
error,
|
|
2315
|
+
readonly,
|
|
2316
|
+
label,
|
|
2317
|
+
path: jsonPath
|
|
2318
|
+
}), /* @__PURE__ */ jsx("div", { children: options && /* @__PURE__ */ jsx(OrderedList.Root, {
|
|
2319
|
+
items: options,
|
|
2320
|
+
isItem: (_item) => true,
|
|
2321
|
+
getId: (option) => option.id,
|
|
2322
|
+
onMove: readonly ? void 0 : handleMove,
|
|
2323
|
+
readonly: !!readonly,
|
|
2324
|
+
children: ({ items }) => /* @__PURE__ */ jsxs(OrderedList.Content, {
|
|
2325
|
+
classNames: "w-full overflow-auto",
|
|
2326
|
+
children: [items?.map((item) => /* @__PURE__ */ jsxs(OrderedList.Item, {
|
|
2327
|
+
id: item.id,
|
|
2328
|
+
item,
|
|
2329
|
+
canDrag: !readonly,
|
|
2330
|
+
classNames: "flex flex-col cursor-pointer rounded-xs dx-hover",
|
|
2331
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2332
|
+
className: "flex items-center",
|
|
2333
|
+
children: [
|
|
2334
|
+
/* @__PURE__ */ jsx(OrderedList.DragHandle, {}),
|
|
2335
|
+
/* @__PURE__ */ jsx("div", {
|
|
2336
|
+
className: "flex grow items-center truncate px-2",
|
|
2337
|
+
onClick: () => handleClick(item.id),
|
|
2338
|
+
children: /* @__PURE__ */ jsx(Tag$1, {
|
|
2339
|
+
hue: item.color,
|
|
2340
|
+
children: item.title || ""
|
|
2341
|
+
})
|
|
2342
|
+
}),
|
|
2343
|
+
/* @__PURE__ */ jsx(ToggleIconButton, {
|
|
2344
|
+
iconOnly: true,
|
|
2345
|
+
variant: "ghost",
|
|
2346
|
+
active: selected === item.id,
|
|
2347
|
+
icon: "ph--caret-right--regular",
|
|
2348
|
+
label: t(selected === item.id ? "collapse.label" : "expand.label", { ns: osTranslations }),
|
|
2349
|
+
onClick: () => handleClick(item.id)
|
|
2350
|
+
})
|
|
2351
|
+
]
|
|
2352
|
+
}), selected === item.id && /* @__PURE__ */ jsxs("div", {
|
|
2353
|
+
className: "flex flex-col p-form-padding gap-form-gap dx-density-md",
|
|
2354
|
+
children: [/* @__PURE__ */ jsx(Input.Label, {
|
|
2355
|
+
classNames: "text-sm",
|
|
2356
|
+
children: t("select-option.label")
|
|
2357
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2358
|
+
className: "grid grid-cols-[1fr_min-content_min-content] gap-form-gap",
|
|
2359
|
+
children: [
|
|
2360
|
+
/* @__PURE__ */ jsx(Input.TextInput, {
|
|
2361
|
+
disabled: !!readonly,
|
|
2362
|
+
placeholder: t("select-option-label.placeholder"),
|
|
2363
|
+
ref: selected === item.id ? inputRef : void 0,
|
|
2364
|
+
value: item.title,
|
|
2365
|
+
onChange: handleTitleChange(item.id),
|
|
2366
|
+
onKeyDown: handleKeyDown,
|
|
2367
|
+
classNames: "flex-1"
|
|
2368
|
+
}),
|
|
2369
|
+
/* @__PURE__ */ jsx(HuePicker, {
|
|
2370
|
+
disabled: !!readonly,
|
|
2371
|
+
value: item.color,
|
|
2372
|
+
onChange: handleColorChange(item.id)
|
|
2373
|
+
}),
|
|
2374
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
2375
|
+
disabled: !!readonly,
|
|
2376
|
+
icon: "ph--trash--fill",
|
|
2377
|
+
iconOnly: true,
|
|
2378
|
+
label: t("select-option-delete.button"),
|
|
2379
|
+
onClick: () => handleDelete(item.id)
|
|
2380
|
+
})
|
|
2381
|
+
]
|
|
2382
|
+
})]
|
|
2383
|
+
})]
|
|
2384
|
+
}, item.id)), /* @__PURE__ */ jsx(IconButton, {
|
|
2385
|
+
classNames: "w-full",
|
|
2386
|
+
variant: "ghost",
|
|
2387
|
+
icon: "ph--plus--regular",
|
|
2388
|
+
label: t("select-option-add.button"),
|
|
2389
|
+
onClick: handleAdd,
|
|
2390
|
+
disabled: !!readonly
|
|
2391
|
+
})]
|
|
2392
|
+
})
|
|
2393
|
+
}) })]
|
|
2394
|
+
});
|
|
2395
|
+
};
|
|
2396
|
+
//#endregion
|
|
2397
|
+
//#region src/components/Form/FormField/fields/TextAreaField/TextAreaField.tsx
|
|
2398
|
+
var TextAreaField = ({ type, readonly, placeholder, onValueChange, onBlur, ...props }) => {
|
|
2399
|
+
const handleChange = useCallback((event) => onValueChange(type, event.target.value), [type, onValueChange]);
|
|
2400
|
+
return /* @__PURE__ */ jsx(FormRow, {
|
|
2401
|
+
readonly,
|
|
2402
|
+
...props,
|
|
2403
|
+
children: ({ value = "" }) => /* @__PURE__ */ jsx(Input.TextArea, {
|
|
2404
|
+
rows: 5,
|
|
2405
|
+
disabled: !!readonly,
|
|
2406
|
+
placeholder,
|
|
2407
|
+
value,
|
|
2408
|
+
onChange: handleChange,
|
|
2409
|
+
onBlur
|
|
2410
|
+
})
|
|
2411
|
+
});
|
|
2412
|
+
};
|
|
2413
|
+
//#endregion
|
|
2414
|
+
//#region src/components/Form/FormField/fields/TupleField/TupleField.tsx
|
|
2415
|
+
var gridCols = [
|
|
2416
|
+
"grid-cols-1",
|
|
2417
|
+
"grid-cols-2",
|
|
2418
|
+
"grid-cols-3",
|
|
2419
|
+
"grid-cols-4"
|
|
2420
|
+
];
|
|
2421
|
+
var TupleField = ({ binding, type, readonly, label, jsonPath, getStatus, getValue, onValueChange, onBlur }) => {
|
|
2422
|
+
const { status, error } = getStatus();
|
|
2423
|
+
const values = getValue() ?? {};
|
|
2424
|
+
return /* @__PURE__ */ jsxs(Input.Root, {
|
|
2425
|
+
validationValence: status,
|
|
2426
|
+
children: [/* @__PURE__ */ jsx(FormFieldLabel, {
|
|
2427
|
+
error,
|
|
2428
|
+
readonly,
|
|
2429
|
+
label,
|
|
2430
|
+
path: jsonPath
|
|
2431
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2432
|
+
className: mx("grid gap-form-gap", gridCols[binding.length - 1]),
|
|
2433
|
+
children: binding.map((prop) => /* @__PURE__ */ jsx(Input.TextInput, {
|
|
2434
|
+
type: "number",
|
|
2435
|
+
disabled: !!readonly,
|
|
2436
|
+
value: values[prop],
|
|
2437
|
+
onChange: (event) => {
|
|
2438
|
+
onValueChange(type, {
|
|
2439
|
+
...values,
|
|
2440
|
+
[prop]: safeParseFloat(event.target.value, 0)
|
|
2441
|
+
});
|
|
2442
|
+
},
|
|
2443
|
+
onBlur
|
|
2444
|
+
}, prop))
|
|
2445
|
+
})]
|
|
2446
|
+
});
|
|
2447
|
+
};
|
|
2448
|
+
//#endregion
|
|
2449
|
+
//#region src/components/Form/FormLayout/resolve-layout-field.ts
|
|
2450
|
+
/**
|
|
2451
|
+
* Walks a dotted path of property names through nested struct ASTs, returning the
|
|
2452
|
+
* leaf `PropertySignature` (or `undefined` if any segment is missing).
|
|
2453
|
+
*/
|
|
2454
|
+
var resolvePropertySignature = (ast, segments) => {
|
|
2455
|
+
let node = ast;
|
|
2456
|
+
for (let index = 0; index < segments.length; index++) {
|
|
2457
|
+
const typeLiteral = SchemaEx.findNode(node, SchemaAST.isTypeLiteral);
|
|
2458
|
+
if (!typeLiteral) return;
|
|
2459
|
+
const prop = SchemaAST.getPropertySignatures(typeLiteral).find((p) => globalThis.String(p.name) === segments[index]);
|
|
2460
|
+
if (!prop) return;
|
|
2461
|
+
if (index === segments.length - 1) return prop;
|
|
2462
|
+
node = prop.type;
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
/**
|
|
2466
|
+
* Resolves a (possibly dotted) layout field name against a schema.
|
|
2467
|
+
* - `origin.code` drills through nested structs to the leaf field.
|
|
2468
|
+
* - A name resolving to a nested struct with a `LabelAnnotation` is flagged via
|
|
2469
|
+
* `labelType` so the layout can auto-convert it to a single read-only label.
|
|
2470
|
+
* Returns `undefined` when the name does not resolve to a property.
|
|
2471
|
+
*/
|
|
2472
|
+
var resolveLayoutField = (schema, name) => {
|
|
2473
|
+
const segments = name.split(".");
|
|
2474
|
+
const prop = resolvePropertySignature(schema.ast, segments);
|
|
2475
|
+
if (!prop) return;
|
|
2476
|
+
const { type: baseType } = SchemaEx.getBaseType(prop);
|
|
2477
|
+
const type = prop.annotations && Reflect.ownKeys(prop.annotations).length > 0 ? {
|
|
2478
|
+
...baseType,
|
|
2479
|
+
annotations: {
|
|
2480
|
+
...baseType.annotations,
|
|
2481
|
+
...prop.annotations
|
|
2482
|
+
}
|
|
2483
|
+
} : baseType;
|
|
2484
|
+
const title = SchemaEx.getAnnotation(SchemaAST.TitleAnnotationId)(type);
|
|
2485
|
+
const labelType = SchemaEx.findNode(prop.type, SchemaAST.isTypeLiteral);
|
|
2486
|
+
const labelled = labelType != null && Option.isSome(Annotation.LabelAnnotation.getFromAst(labelType));
|
|
2487
|
+
return {
|
|
2488
|
+
type,
|
|
2489
|
+
segments,
|
|
2490
|
+
leafName: segments[segments.length - 1],
|
|
2491
|
+
title,
|
|
2492
|
+
labelType: labelled ? labelType : void 0,
|
|
2493
|
+
required: !prop.isOptional
|
|
2494
|
+
};
|
|
2495
|
+
};
|
|
2496
|
+
//#endregion
|
|
2497
|
+
//#region src/components/Form/FormLayout/parser.ts
|
|
2498
|
+
var LayoutParseError = class extends Error {
|
|
2499
|
+
position;
|
|
2500
|
+
_tag = "LayoutParseError";
|
|
2501
|
+
constructor(message, position) {
|
|
2502
|
+
super(position !== void 0 ? `${message} (at position ${position})` : message);
|
|
2503
|
+
this.position = position;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
var TAG_RE = /<\s*(\/?)\s*(\w+)([^>]*?)(\/?)\s*>/g;
|
|
2507
|
+
var ATTR_RE = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\d+))/g;
|
|
2508
|
+
var parseAttrs = (raw) => {
|
|
2509
|
+
const attrs = {};
|
|
2510
|
+
ATTR_RE.lastIndex = 0;
|
|
2511
|
+
let match;
|
|
2512
|
+
while ((match = ATTR_RE.exec(raw)) !== null) {
|
|
2513
|
+
const [, key, doubleQuoted, singleQuoted, bare] = match;
|
|
2514
|
+
attrs[key] = doubleQuoted ?? singleQuoted ?? bare;
|
|
2515
|
+
}
|
|
2516
|
+
return attrs;
|
|
2517
|
+
};
|
|
2518
|
+
var tokenize = (input) => {
|
|
2519
|
+
const tokens = [];
|
|
2520
|
+
TAG_RE.lastIndex = 0;
|
|
2521
|
+
let match;
|
|
2522
|
+
let cursor = 0;
|
|
2523
|
+
while ((match = TAG_RE.exec(input)) !== null) {
|
|
2524
|
+
const between = input.slice(cursor, match.index);
|
|
2525
|
+
if (between.trim().length > 0) throw new LayoutParseError(`unexpected text content: ${JSON.stringify(between.trim())}`, cursor);
|
|
2526
|
+
const [, slash, name, attrsRaw, selfSlash] = match;
|
|
2527
|
+
if (slash) tokens.push({
|
|
2528
|
+
type: "close",
|
|
2529
|
+
name,
|
|
2530
|
+
position: match.index
|
|
2531
|
+
});
|
|
2532
|
+
else tokens.push({
|
|
2533
|
+
type: "open",
|
|
2534
|
+
name,
|
|
2535
|
+
attrs: parseAttrs(attrsRaw),
|
|
2536
|
+
selfClosing: selfSlash === "/",
|
|
2537
|
+
position: match.index
|
|
2538
|
+
});
|
|
2539
|
+
cursor = match.index + match[0].length;
|
|
2540
|
+
}
|
|
2541
|
+
const trailing = input.slice(cursor);
|
|
2542
|
+
if (trailing.trim().length > 0) throw new LayoutParseError(`unexpected text content: ${JSON.stringify(trailing.trim())}`, cursor);
|
|
2543
|
+
return tokens;
|
|
2544
|
+
};
|
|
2545
|
+
var requireAttr = (attrs, key, tag, position) => {
|
|
2546
|
+
const value = attrs[key];
|
|
2547
|
+
if (value === void 0) throw new LayoutParseError(`<${tag}> missing required attribute "${key}"`, position);
|
|
2548
|
+
return value;
|
|
2549
|
+
};
|
|
2550
|
+
var parseInteger = (value, attr, tag, position) => {
|
|
2551
|
+
if (!/^\d+$/.test(value)) throw new LayoutParseError(`<${tag}> attribute "${attr}" expects an integer, got ${JSON.stringify(value)}`, position);
|
|
2552
|
+
return Number.parseInt(value, 10);
|
|
2553
|
+
};
|
|
2554
|
+
/** Parses a layout template string into a single root node. The root must be `<grid>`. */
|
|
2555
|
+
var parseLayout = (template) => {
|
|
2556
|
+
const tokens = tokenize(template);
|
|
2557
|
+
if (tokens.length === 0) throw new LayoutParseError("empty template");
|
|
2558
|
+
let index = 0;
|
|
2559
|
+
const parseNode = () => {
|
|
2560
|
+
const token = tokens[index];
|
|
2561
|
+
if (!token || token.type !== "open") throw new LayoutParseError(`expected an opening tag, got ${JSON.stringify(token)}`, token?.position);
|
|
2562
|
+
if (token.name === "field") {
|
|
2563
|
+
if (!token.selfClosing) throw new LayoutParseError("<field> must be self-closing (e.g. <field name=\"x\"/>)", token.position);
|
|
2564
|
+
const name = requireAttr(token.attrs, "name", "field", token.position);
|
|
2565
|
+
const spanRaw = token.attrs.span;
|
|
2566
|
+
const span = spanRaw !== void 0 ? parseInteger(spanRaw, "span", "field", token.position) : void 0;
|
|
2567
|
+
index += 1;
|
|
2568
|
+
return {
|
|
2569
|
+
kind: "field",
|
|
2570
|
+
name,
|
|
2571
|
+
span
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
if (token.name === "grid") {
|
|
2575
|
+
if (token.selfClosing) throw new LayoutParseError("<grid> must wrap children (cannot self-close)", token.position);
|
|
2576
|
+
const cols = parseInteger(requireAttr(token.attrs, "cols", "grid", token.position), "cols", "grid", token.position);
|
|
2577
|
+
index += 1;
|
|
2578
|
+
const children = [];
|
|
2579
|
+
while (index < tokens.length && !(tokens[index].type === "close" && tokens[index].name === "grid")) children.push(parseNode());
|
|
2580
|
+
const closing = tokens[index];
|
|
2581
|
+
if (!closing || closing.type !== "close" || closing.name !== "grid") throw new LayoutParseError("<grid> not closed", token.position);
|
|
2582
|
+
index += 1;
|
|
2583
|
+
return {
|
|
2584
|
+
kind: "grid",
|
|
2585
|
+
cols,
|
|
2586
|
+
children
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
throw new LayoutParseError(`unknown element <${token.name}>`, token.position);
|
|
2590
|
+
};
|
|
2591
|
+
const root = parseNode();
|
|
2592
|
+
if (index !== tokens.length) {
|
|
2593
|
+
const trailing = tokens[index];
|
|
2594
|
+
throw new LayoutParseError(`unexpected trailing tag <${trailing.type === "open" ? "" : "/"}${trailing.name}>`, trailing.position);
|
|
2595
|
+
}
|
|
2596
|
+
if (root.kind !== "grid") throw new LayoutParseError("root element must be <grid>");
|
|
2597
|
+
return root;
|
|
2598
|
+
};
|
|
2599
|
+
//#endregion
|
|
2600
|
+
//#region src/components/Form/FormLayout/FormLayout.tsx
|
|
2601
|
+
var FORM_LAYOUT_NAME$1 = "Form.Layout";
|
|
2602
|
+
/**
|
|
2603
|
+
* Lays out schema fields according to a `FormLayout` DSL template. The template
|
|
2604
|
+
* is either passed via the `template` prop or read from the schema's
|
|
2605
|
+
* `FormLayoutAnnotation`. Fields not referenced in the template are hidden —
|
|
2606
|
+
* the template controls exactly what renders.
|
|
2607
|
+
*
|
|
2608
|
+
* A `<field name=…/>` resolves against the schema:
|
|
2609
|
+
* - A dotted name (`origin.code`) drills into a nested struct and renders the leaf field.
|
|
2610
|
+
* - A name that resolves to a nested struct carrying a `LabelAnnotation` auto-converts
|
|
2611
|
+
* to the struct's computed label, rendered as a single read-only text (rather than
|
|
2612
|
+
* expanding the struct's sub-fields).
|
|
2613
|
+
*/
|
|
2614
|
+
var FormLayout = ({ schema, template, name = Annotation.DEFAULT_LAYOUT_NAME, path, readonly, layout, projection, ...props }) => {
|
|
2615
|
+
const annotated = Option.getOrUndefined(Annotation.FormLayoutAnnotation.get(schema));
|
|
2616
|
+
const source = template ?? annotated?.[name];
|
|
2617
|
+
if (source === void 0) throw new LayoutParseError(annotated ? `no layout named "${name}" on schema (available: ${Object.keys(annotated).join(", ") || "∅"})` : `no template provided and schema has no FormLayoutAnnotation`);
|
|
2618
|
+
return /* @__PURE__ */ jsx(RenderNode, {
|
|
2619
|
+
node: useMemo(() => parseLayout(source), [source]),
|
|
2620
|
+
schema,
|
|
2621
|
+
basePath: path ?? [],
|
|
2622
|
+
readonly,
|
|
2623
|
+
layout,
|
|
2624
|
+
projection,
|
|
2625
|
+
...props
|
|
2626
|
+
});
|
|
2627
|
+
};
|
|
2628
|
+
FormLayout.displayName = FORM_LAYOUT_NAME$1;
|
|
2629
|
+
var RenderNode = ({ node, schema, basePath, ...props }) => {
|
|
2630
|
+
if (node.kind === "grid") return /* @__PURE__ */ jsx("div", {
|
|
2631
|
+
className: "grid gap-x-form-gap",
|
|
2632
|
+
style: { gridTemplateColumns: `repeat(${node.cols}, minmax(0, 1fr))` },
|
|
2633
|
+
children: node.children.map((child, index) => /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(RenderNode, {
|
|
2634
|
+
node: child,
|
|
2635
|
+
schema,
|
|
2636
|
+
basePath,
|
|
2637
|
+
...props
|
|
2638
|
+
}) }, index))
|
|
2639
|
+
});
|
|
2640
|
+
const resolved = resolveLayoutField(schema, node.name);
|
|
2641
|
+
if (!resolved) throw new LayoutParseError(`field "${node.name}" not found on schema`);
|
|
2642
|
+
const { type, segments, leafName, title, labelType, required } = resolved;
|
|
2643
|
+
const path = [...basePath, ...segments];
|
|
2644
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2645
|
+
className: "min-w-0",
|
|
2646
|
+
style: node.span ? { gridColumn: `span ${node.span} / span ${node.span}` } : void 0,
|
|
2647
|
+
children: /* @__PURE__ */ jsx(FormFieldErrorBoundary, {
|
|
2648
|
+
path,
|
|
2649
|
+
children: labelType ? /* @__PURE__ */ jsx(LabelField, {
|
|
2650
|
+
schema: Schema.make(labelType),
|
|
2651
|
+
label: title ?? String$1.capitalize(leafName),
|
|
2652
|
+
path,
|
|
2653
|
+
layout: props.layout
|
|
2654
|
+
}) : /* @__PURE__ */ jsx(FormField, {
|
|
2655
|
+
type,
|
|
2656
|
+
name: leafName,
|
|
2657
|
+
path,
|
|
2658
|
+
required,
|
|
2659
|
+
...props
|
|
2660
|
+
})
|
|
2661
|
+
})
|
|
2662
|
+
});
|
|
2663
|
+
};
|
|
2664
|
+
/**
|
|
2665
|
+
* Renders a nested struct value as its computed label (via `LabelAnnotation`),
|
|
2666
|
+
* as a single read-only text. Empty values are omitted, mirroring the static
|
|
2667
|
+
* presentation of regular fields.
|
|
2668
|
+
*/
|
|
2669
|
+
var LabelField = ({ schema, label, path, layout }) => {
|
|
2670
|
+
const { variant = "default" } = useFormContext(FORM_LAYOUT_NAME$1);
|
|
2671
|
+
const styles = formTheme.styles({ variant });
|
|
2672
|
+
const { getValue } = useFormFieldState(FORM_LAYOUT_NAME$1, path);
|
|
2673
|
+
const value = getValue();
|
|
2674
|
+
const text = value == null ? void 0 : Annotation.getLabelWithSchema(schema, value);
|
|
2675
|
+
if (text == null || text.trim().length === 0) return null;
|
|
2676
|
+
const presentation = presentationFor(layout);
|
|
2677
|
+
return /* @__PURE__ */ jsx(Input.Root, { children: /* @__PURE__ */ jsxs("div", {
|
|
2678
|
+
className: styles.field(),
|
|
2679
|
+
children: [presentation.showLabel && /* @__PURE__ */ jsx(FormFieldLabel, {
|
|
2680
|
+
variant,
|
|
2681
|
+
readonly: true,
|
|
2682
|
+
label,
|
|
2683
|
+
path: SchemaEx.createJsonPath(path)
|
|
2684
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2685
|
+
className: styles.fieldControl(),
|
|
2686
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
2687
|
+
className: "truncate min-w-0",
|
|
2688
|
+
title: text,
|
|
2689
|
+
children: text
|
|
2690
|
+
})
|
|
2691
|
+
})]
|
|
2692
|
+
}) });
|
|
2693
|
+
};
|
|
2694
|
+
//#endregion
|
|
2695
|
+
//#region src/components/Form/FormFieldSet/FormFieldSetContainer.tsx
|
|
2696
|
+
var styles$1 = formTheme.styles();
|
|
2697
|
+
/**
|
|
2698
|
+
* Shared chrome for a labelled group of sub-fields: an optional header (label + collapse toggle) and,
|
|
2699
|
+
* when collapsible, an indented bordered container around the body. Nested structs, object-array items,
|
|
2700
|
+
* and inline refs all reach this via `FormFieldSet`, so their visual containment is identical.
|
|
2701
|
+
*/
|
|
2702
|
+
var FormFieldSetContainer = ({ classNames, label, path, readonly, presentation, collapsible, children }) => {
|
|
2703
|
+
const { t } = useTranslation(translationKey);
|
|
2704
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
2705
|
+
const canCollapse = collapsible && Children.toArray(children).length > 0;
|
|
2706
|
+
const showBody = !(canCollapse && collapsed);
|
|
2707
|
+
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [presentation.showLabel && label && /* @__PURE__ */ jsx(FormFieldHeader, {
|
|
2708
|
+
label,
|
|
2709
|
+
path,
|
|
2710
|
+
readonly,
|
|
2711
|
+
classNames: "pl-2",
|
|
2712
|
+
actions: canCollapse ? /* @__PURE__ */ jsx(ToggleIconButton, {
|
|
2713
|
+
active: !collapsed,
|
|
2714
|
+
classNames: "px-1 mr-0.5",
|
|
2715
|
+
variant: "ghost",
|
|
2716
|
+
density: "sm",
|
|
2717
|
+
iconOnly: true,
|
|
2718
|
+
icon: "ph--caret-right--regular",
|
|
2719
|
+
label: t(collapsed ? "expand-fields.label" : "collapse-fields.label")
|
|
2720
|
+
}) : void 0,
|
|
2721
|
+
onClick: canCollapse ? () => setCollapsed((value) => !value) : void 0
|
|
2722
|
+
}), showBody && (canCollapse ? /* @__PURE__ */ jsx("div", {
|
|
2723
|
+
className: styles$1.fieldSetBody(),
|
|
2724
|
+
children
|
|
2725
|
+
}) : children)] });
|
|
2726
|
+
if (canCollapse) return /* @__PURE__ */ jsx("div", {
|
|
2727
|
+
className: styles$1.fieldSetBoxOuter(),
|
|
2728
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2729
|
+
className: styles$1.fieldSetBox({ class: mx(classNames) }),
|
|
2730
|
+
children: content
|
|
2731
|
+
})
|
|
2732
|
+
});
|
|
2733
|
+
return classNames ? /* @__PURE__ */ jsx("div", {
|
|
2734
|
+
className: mx(classNames),
|
|
2735
|
+
children: content
|
|
2736
|
+
}) : /* @__PURE__ */ jsx(Fragment$1, { children: content });
|
|
2737
|
+
};
|
|
2738
|
+
//#endregion
|
|
2739
|
+
//#region src/components/Form/FormFieldSet/FormFieldSet.tsx
|
|
2740
|
+
var FORM_FIELDSET_NAME$1 = "Form.FieldSet";
|
|
2741
|
+
/**
|
|
2742
|
+
* Renders a set of form fields derived from a schema object.
|
|
2743
|
+
*/
|
|
2744
|
+
var FormFieldSet = ({ classNames, label, schema, readonly, path, sort, collapsible, filter, projection, layout, layoutName = DEFAULT_LAYOUT_NAME, ...props }) => {
|
|
2745
|
+
const values = useFormValues(FORM_FIELDSET_NAME$1, path);
|
|
2746
|
+
const properties = useFormFieldSetProperties({
|
|
2747
|
+
schema,
|
|
2748
|
+
values,
|
|
2749
|
+
filter,
|
|
2750
|
+
sort,
|
|
2751
|
+
projection
|
|
2752
|
+
});
|
|
2753
|
+
if ((readonly || layout === "static") && values == null) return null;
|
|
2754
|
+
const body = (schema ? Option.getOrUndefined(FormLayoutAnnotation.get(schema)) : void 0)?.[layoutName] !== void 0 && schema ? /* @__PURE__ */ jsx(FormLayout, {
|
|
2755
|
+
schema,
|
|
2756
|
+
name: layoutName,
|
|
2757
|
+
path,
|
|
2758
|
+
readonly,
|
|
2759
|
+
layout,
|
|
2760
|
+
projection,
|
|
2761
|
+
...props
|
|
2762
|
+
}) : properties.map((property) => {
|
|
2763
|
+
const name = property.name.toString();
|
|
2764
|
+
return /* @__PURE__ */ jsx(FormFieldErrorBoundary, {
|
|
2765
|
+
path: [...path ?? [], name],
|
|
2766
|
+
children: /* @__PURE__ */ jsx(FormField, {
|
|
2767
|
+
type: property.type,
|
|
2768
|
+
name,
|
|
2769
|
+
path: [...path ?? [], name],
|
|
2770
|
+
required: !property.isOptional,
|
|
2771
|
+
readonly,
|
|
2772
|
+
layout,
|
|
2773
|
+
projection,
|
|
2774
|
+
...props
|
|
2775
|
+
})
|
|
2776
|
+
}, name);
|
|
2777
|
+
});
|
|
2778
|
+
return /* @__PURE__ */ jsx(FormFieldSetContainer, {
|
|
2779
|
+
classNames,
|
|
2780
|
+
label,
|
|
2781
|
+
path: SchemaEx.createJsonPath(path ?? []),
|
|
2782
|
+
readonly,
|
|
2783
|
+
presentation: presentationFor(layout),
|
|
2784
|
+
collapsible,
|
|
2785
|
+
children: body
|
|
2786
|
+
});
|
|
2787
|
+
};
|
|
2788
|
+
FormFieldSet.displayName = FORM_FIELDSET_NAME$1;
|
|
2789
|
+
/**
|
|
2790
|
+
* Resolves ordered schema properties for a field set (projection order, filter, or sort).
|
|
2791
|
+
*/
|
|
2792
|
+
var useFormFieldSetProperties = ({ schema, filter, projection, values, sort }) => {
|
|
2793
|
+
return useMemo(() => {
|
|
2794
|
+
if (!schema) return [];
|
|
2795
|
+
const schemaProps = getRootFormProperties(schema.ast, values);
|
|
2796
|
+
const filteredProps = filter ? filter(schemaProps) : schemaProps;
|
|
2797
|
+
if (projection) {
|
|
2798
|
+
const fieldProjections = projection.getFieldProjections();
|
|
2799
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
2800
|
+
const visibleProps = filteredProps.filter((prop) => !hiddenProperties.has(prop.name.toString()));
|
|
2801
|
+
const orderedProps = [];
|
|
2802
|
+
for (const fieldProjection of fieldProjections) {
|
|
2803
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
2804
|
+
const prop = visibleProps.find((prop) => prop.name === fieldPath);
|
|
2805
|
+
if (prop) orderedProps.push(prop);
|
|
2806
|
+
}
|
|
2807
|
+
const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
|
|
2808
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
|
|
2809
|
+
orderedProps.push(...remainingProps);
|
|
2810
|
+
return orderedProps;
|
|
2811
|
+
}
|
|
2812
|
+
return sort ? [...filteredProps].sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString())) : filteredProps;
|
|
2813
|
+
}, [
|
|
2814
|
+
schema,
|
|
2815
|
+
values,
|
|
2816
|
+
filter,
|
|
2817
|
+
sort,
|
|
2818
|
+
projection
|
|
2819
|
+
]);
|
|
2820
|
+
};
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region src/components/Form/FormControls.tsx
|
|
2823
|
+
var FormRoot = ({ children, schema, values, onSave, onCancel, ...props }) => {
|
|
2824
|
+
return /* @__PURE__ */ jsx(FormContextProvider, {
|
|
2825
|
+
form: useFormHandler({
|
|
2826
|
+
schema,
|
|
2827
|
+
values,
|
|
2828
|
+
onSave,
|
|
2829
|
+
onCancel,
|
|
2830
|
+
...props
|
|
2831
|
+
}),
|
|
2832
|
+
...props,
|
|
2833
|
+
children
|
|
2834
|
+
});
|
|
2835
|
+
};
|
|
2836
|
+
FormRoot.displayName = "Form.Root";
|
|
2837
|
+
var FORM_VIEWPORT_NAME = "Form.Viewport";
|
|
2838
|
+
var FormViewport = composable(({ children, scroll, gutter = "sm", ...props }, forwardedRef) => {
|
|
2839
|
+
const { variant = "default" } = useFormContext(FORM_VIEWPORT_NAME);
|
|
2840
|
+
const styles = formTheme.styles({ variant });
|
|
2841
|
+
const inColumn = useInColumn();
|
|
2842
|
+
const span = "[.dx-column-root_&]:col-span-full";
|
|
2843
|
+
if (inColumn && !scroll) return /* @__PURE__ */ jsx(Column.Center, {
|
|
2844
|
+
...composableProps(props, { classNames: ["w-full min-w-0", styles.viewport()] }),
|
|
2845
|
+
ref: forwardedRef,
|
|
2846
|
+
children
|
|
2847
|
+
});
|
|
2848
|
+
if (scroll) return /* @__PURE__ */ jsx(Column.Root, {
|
|
2849
|
+
...composableProps(props, { classNames: [
|
|
2850
|
+
"dx-expander",
|
|
2851
|
+
span,
|
|
2852
|
+
styles.viewport()
|
|
2853
|
+
] }),
|
|
2854
|
+
gutter,
|
|
2855
|
+
ref: forwardedRef,
|
|
2856
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Root, {
|
|
2857
|
+
orientation: "vertical",
|
|
2858
|
+
centered: true,
|
|
2859
|
+
padding: true,
|
|
2860
|
+
thin: true,
|
|
2861
|
+
children: /* @__PURE__ */ jsx(ScrollArea.Viewport, { children })
|
|
2862
|
+
})
|
|
2863
|
+
});
|
|
2864
|
+
return /* @__PURE__ */ jsx(Column.Root, {
|
|
2865
|
+
...composableProps(props, { classNames: [
|
|
2866
|
+
"w-full min-w-0",
|
|
2867
|
+
span,
|
|
2868
|
+
styles.viewport()
|
|
2869
|
+
] }),
|
|
2870
|
+
gutter,
|
|
2871
|
+
ref: forwardedRef,
|
|
2872
|
+
children
|
|
2873
|
+
});
|
|
2874
|
+
});
|
|
2875
|
+
FormViewport.displayName = FORM_VIEWPORT_NAME;
|
|
2876
|
+
var FORM_CONTENT_NAME = "Form.Content";
|
|
2877
|
+
var FormContent = composable(({ children, ...props }, forwardedRef) => {
|
|
2878
|
+
const { form, testId, variant = "default" } = useFormContext(FORM_CONTENT_NAME);
|
|
2879
|
+
const styles = formTheme.styles({ variant });
|
|
2880
|
+
const localRef = useRef(null);
|
|
2881
|
+
const mergedRef = useMergeRefs([forwardedRef, localRef]);
|
|
2882
|
+
useKeyHandler(localRef, form);
|
|
2883
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2884
|
+
...composableProps(props, {
|
|
2885
|
+
role: "form",
|
|
2886
|
+
classNames: mx(withColumn.center(), "flex flex-col w-full dx-document", styles.content())
|
|
2887
|
+
}),
|
|
2888
|
+
"data-testid": testId,
|
|
2889
|
+
ref: mergedRef,
|
|
2890
|
+
children
|
|
2891
|
+
});
|
|
2892
|
+
});
|
|
2893
|
+
FormContent.displayName = FORM_CONTENT_NAME;
|
|
2894
|
+
var FORM_FIELDSET_NAME = "Form.FieldSet";
|
|
2895
|
+
/** Context-reading binding for `Form.FieldSet`: pulls the schema + field context off the form and delegates to {@link FormFieldSet}. */
|
|
2896
|
+
var FormFieldSetContainer$1 = ({ classNames, ...props }) => {
|
|
2897
|
+
const { form, variant = "default", ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
|
|
2898
|
+
const styles = formTheme.styles({ variant });
|
|
2899
|
+
return /* @__PURE__ */ jsx(FormFieldSet, {
|
|
2900
|
+
schema: form.schema,
|
|
2901
|
+
classNames: styles.fieldSet({ class: classNames }),
|
|
2902
|
+
...contextProps,
|
|
2903
|
+
...props
|
|
2904
|
+
});
|
|
2905
|
+
};
|
|
2906
|
+
FormFieldSetContainer$1.displayName = FORM_FIELDSET_NAME;
|
|
2907
|
+
var FORM_LAYOUT_NAME = "Form.Layout";
|
|
2908
|
+
/** Context-reading binding for `Form.Layout`: resolves the schema (prop or form) and delegates to {@link FormLayout}. */
|
|
2909
|
+
var FormLayoutController = ({ schema, ...props }) => {
|
|
2910
|
+
const { form, ...contextProps } = useFormContext(FORM_LAYOUT_NAME);
|
|
2911
|
+
const resolvedSchema = schema ?? form.schema;
|
|
2912
|
+
if (!resolvedSchema) return null;
|
|
2913
|
+
return /* @__PURE__ */ jsx(FormLayout, {
|
|
2914
|
+
schema: resolvedSchema,
|
|
2915
|
+
...contextProps,
|
|
2916
|
+
...props
|
|
2917
|
+
});
|
|
2918
|
+
};
|
|
2919
|
+
FormLayoutController.displayName = FORM_LAYOUT_NAME;
|
|
2920
|
+
var FORM_ACTIONS_NAME = "Form.Actions";
|
|
2921
|
+
var FormActions = ({ classNames }) => {
|
|
2922
|
+
const { t } = useTranslation(translationKey);
|
|
2923
|
+
const { form: { canSave, onSave, onCancel }, readonly, layout } = useFormContext(FORM_ACTIONS_NAME);
|
|
2924
|
+
if (readonly || layout === "static") return null;
|
|
2925
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2926
|
+
className: mx(withColumn.center(), formTheme.styles().actions(), classNames),
|
|
2927
|
+
children: [onCancel && /* @__PURE__ */ jsx(IconButton, {
|
|
2928
|
+
icon: "ph--x--regular",
|
|
2929
|
+
iconEnd: true,
|
|
2930
|
+
label: t("cancel-button.label"),
|
|
2931
|
+
onClick: onCancel,
|
|
2932
|
+
"data-testid": "cancel-button"
|
|
2933
|
+
}), onSave && /* @__PURE__ */ jsx(IconButton, {
|
|
2934
|
+
type: "submit",
|
|
2935
|
+
variant: "primary",
|
|
2936
|
+
disabled: !canSave,
|
|
2937
|
+
icon: "ph--check--regular",
|
|
2938
|
+
iconEnd: true,
|
|
2939
|
+
label: t("save-button.label"),
|
|
2940
|
+
onClick: onSave,
|
|
2941
|
+
"data-testid": "save-button"
|
|
2942
|
+
})]
|
|
2943
|
+
});
|
|
2944
|
+
};
|
|
2945
|
+
FormActions.displayName = FORM_ACTIONS_NAME;
|
|
2946
|
+
var FORM_SECTION_NAME = "Form.Section";
|
|
2947
|
+
var FormSection = composable(({ children, title, description, ...props }, forwardedRef) => {
|
|
2948
|
+
const { variant = "default" } = useFormContext(FORM_SECTION_NAME);
|
|
2949
|
+
const styles = formTheme.styles({ variant });
|
|
2950
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2951
|
+
...composableProps(props, { classNames: styles.section() }),
|
|
2952
|
+
ref: forwardedRef,
|
|
2953
|
+
children: [(title || description) && /* @__PURE__ */ jsxs("div", {
|
|
2954
|
+
className: styles.sectionHeader(),
|
|
2955
|
+
children: [title && /* @__PURE__ */ jsx("h2", {
|
|
2956
|
+
className: styles.sectionTitle(),
|
|
2957
|
+
children: title
|
|
2958
|
+
}), description && /* @__PURE__ */ jsx(MarkdownView, {
|
|
2959
|
+
classNames: styles.sectionDescription(),
|
|
2960
|
+
content: description
|
|
2961
|
+
})]
|
|
2962
|
+
}), children]
|
|
2963
|
+
});
|
|
2964
|
+
});
|
|
2965
|
+
FormSection.displayName = FORM_SECTION_NAME;
|
|
2966
|
+
var FORM_GROUP_NAME = "Form.Group";
|
|
2967
|
+
/**
|
|
2968
|
+
* A bordered card grouping related rows/controls (e.g. an entity card in a list).
|
|
2969
|
+
* Layout-only and context-free — unlike `Form.FieldSet` it is not schema-driven.
|
|
2970
|
+
*/
|
|
2971
|
+
var FormGroup = composable(({ children, ...props }, forwardedRef) => {
|
|
2972
|
+
const { variant = "default" } = useFormContext(FORM_SECTION_NAME);
|
|
2973
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2974
|
+
...composableProps(props, { classNames: formTheme.styles({ variant }).group() }),
|
|
2975
|
+
ref: forwardedRef,
|
|
2976
|
+
children
|
|
2977
|
+
});
|
|
2978
|
+
});
|
|
2979
|
+
FormGroup.displayName = FORM_GROUP_NAME;
|
|
2980
|
+
var FORM_SUBMIT_NAME = "Form.Submit";
|
|
2981
|
+
var FormSubmit = ({ classNames, label, icon, disabled }) => {
|
|
2982
|
+
const { t } = useTranslation(translationKey);
|
|
2983
|
+
const { form: { canSave, onSave }, readonly, layout } = useFormContext(FORM_SUBMIT_NAME);
|
|
2984
|
+
if (readonly || layout === "static") return null;
|
|
2985
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2986
|
+
className: mx(formTheme.styles().submit(), classNames),
|
|
2987
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
2988
|
+
classNames: "w-full",
|
|
2989
|
+
type: "submit",
|
|
2990
|
+
variant: "primary",
|
|
2991
|
+
disabled: disabled ?? !canSave,
|
|
2992
|
+
icon: icon ?? "ph--check--regular",
|
|
2993
|
+
label: label ?? t("save-button.label"),
|
|
2994
|
+
onClick: onSave,
|
|
2995
|
+
"data-testid": "save-button"
|
|
2996
|
+
})
|
|
2997
|
+
});
|
|
2998
|
+
};
|
|
2999
|
+
FormSubmit.displayName = FORM_SUBMIT_NAME;
|
|
3000
|
+
var FORM_ERROR_NAME = "Form.Error";
|
|
3001
|
+
/** Form-level error/validation message (e.g. a failed submit), styled via the error valence. */
|
|
3002
|
+
var FormError = ({ children, classNames }) => {
|
|
3003
|
+
if (!children) return null;
|
|
3004
|
+
return /* @__PURE__ */ jsx(Input.Root, {
|
|
3005
|
+
validationValence: "error",
|
|
3006
|
+
children: /* @__PURE__ */ jsx(Input.Validation, {
|
|
3007
|
+
classNames,
|
|
3008
|
+
children
|
|
3009
|
+
})
|
|
3010
|
+
});
|
|
3011
|
+
};
|
|
3012
|
+
FormError.displayName = FORM_ERROR_NAME;
|
|
3013
|
+
//#endregion
|
|
3014
|
+
//#region src/components/Form/Form.tsx
|
|
3015
|
+
var Form = {
|
|
3016
|
+
Root: FormRoot,
|
|
3017
|
+
Viewport: FormViewport,
|
|
3018
|
+
Content: FormContent,
|
|
3019
|
+
Section: FormSection,
|
|
3020
|
+
Group: FormGroup,
|
|
3021
|
+
FieldSet: FormFieldSetContainer$1,
|
|
3022
|
+
Layout: FormLayoutController,
|
|
3023
|
+
Label: FormFieldLabel,
|
|
3024
|
+
Row: FormRow,
|
|
3025
|
+
Actions: FormActions,
|
|
3026
|
+
Submit: FormSubmit,
|
|
3027
|
+
Error: FormError
|
|
3028
|
+
};
|
|
3029
|
+
//#endregion
|
|
3030
|
+
//#region src/components/FieldEditor/FieldEditor.tsx
|
|
3031
|
+
var __dxlog_file$3 = "/__w/dxos/dxos/packages/ui/react-ui-form/src/components/FieldEditor/FieldEditor.tsx";
|
|
3032
|
+
/**
|
|
3033
|
+
* Displays a Form representing the metadata for a `Field` within a given `View`.
|
|
3034
|
+
*/
|
|
3035
|
+
var FieldEditor = ({ readonly, projection, field, registry, view, onSave, onCancel }) => {
|
|
3036
|
+
const { t } = useTranslation(translationKey);
|
|
3037
|
+
const [props, setProps] = useState(projection.getFieldProjection(field.id).props);
|
|
3038
|
+
useEffect(() => setProps(projection.getFieldProjection(field.id).props), [field, projection]);
|
|
3039
|
+
const [schemas, setSchemas] = useState([]);
|
|
3040
|
+
useAsyncEffect(async () => {
|
|
3041
|
+
if (!registry) return;
|
|
3042
|
+
const subscription = registry.query(Filter.type(Type.Type)).subscribe((query) => setSchemas(query.results), { fire: true });
|
|
3043
|
+
const schemas = await registry.query(Filter.type(Type.Type)).run();
|
|
3044
|
+
setSchemas(schemas);
|
|
3045
|
+
return () => subscription?.();
|
|
3046
|
+
}, [registry]);
|
|
3047
|
+
const [referenceSchema, setReferenceSchema] = useState();
|
|
3048
|
+
useEffect(() => {
|
|
3049
|
+
setReferenceSchema(() => schemas.find((schema) => Type.getTypename(schema) === props?.referenceSchema));
|
|
3050
|
+
}, [schemas, props?.referenceSchema]);
|
|
3051
|
+
const [{ fieldSchema }, setFieldSchema] = useState({ fieldSchema: getFormatSchema(props?.format) });
|
|
3052
|
+
const fieldMap = useMemo(() => ({
|
|
3053
|
+
["format"]: (props) => /* @__PURE__ */ jsx(SelectField, {
|
|
3054
|
+
...props,
|
|
3055
|
+
options: FormatEnums.filter((value) => value !== Format$1.TypeFormat.None).map((value) => ({
|
|
3056
|
+
value,
|
|
3057
|
+
label: t(`format.${value}.label`)
|
|
3058
|
+
}))
|
|
3059
|
+
}),
|
|
3060
|
+
["referenceSchema"]: (props) => /* @__PURE__ */ jsx(SelectField, {
|
|
3061
|
+
...props,
|
|
3062
|
+
options: schemas.map((schema) => Type.getTypename(schema)).filter((typename) => typename != null).map((typename) => ({ value: typename }))
|
|
3063
|
+
}),
|
|
3064
|
+
["referencePath"]: (props) => /* @__PURE__ */ jsx(SelectField, {
|
|
3065
|
+
...props,
|
|
3066
|
+
options: referenceSchema ? getFormProperties(Type.getSchema(referenceSchema).ast).sort((a, b) => a.name.toString().localeCompare(b.name.toString())).map((p) => ({ value: p.name.toString() })) : []
|
|
3067
|
+
}),
|
|
3068
|
+
["options"]: (props) => /* @__PURE__ */ jsx(SelectOptionField, { ...props })
|
|
3069
|
+
}), [
|
|
3070
|
+
t,
|
|
3071
|
+
schemas,
|
|
3072
|
+
referenceSchema
|
|
3073
|
+
]);
|
|
3074
|
+
const propIsNotType = useCallback((props) => props.filter((prop) => prop.name !== "type"), []);
|
|
3075
|
+
const handleValuesChanged = useCallback((_props) => {
|
|
3076
|
+
setFieldSchema((prev) => {
|
|
3077
|
+
const fieldSchema = getFormatSchema(_props.format);
|
|
3078
|
+
if (prev.fieldSchema === fieldSchema) return prev;
|
|
3079
|
+
return { fieldSchema };
|
|
3080
|
+
});
|
|
3081
|
+
setReferenceSchema((prev) => {
|
|
3082
|
+
if (_props.referenceSchema !== (prev ? Type.getTypename(prev) : void 0)) {
|
|
3083
|
+
const newSchema = schemas.find((schema) => Type.getTypename(schema) === _props.referenceSchema);
|
|
3084
|
+
if (newSchema) return newSchema;
|
|
3085
|
+
}
|
|
3086
|
+
return prev;
|
|
3087
|
+
});
|
|
3088
|
+
setProps((props) => {
|
|
3089
|
+
const type = formatToType[_props.format];
|
|
3090
|
+
const additionalProps = formatToAdditionalPropertyAttributes[_props.format];
|
|
3091
|
+
if (props.type !== type) return {
|
|
3092
|
+
...props,
|
|
3093
|
+
..._props,
|
|
3094
|
+
...additionalProps,
|
|
3095
|
+
type
|
|
3096
|
+
};
|
|
3097
|
+
return _props;
|
|
3098
|
+
});
|
|
3099
|
+
}, [
|
|
3100
|
+
schemas,
|
|
3101
|
+
props.format,
|
|
3102
|
+
props.referenceSchema
|
|
3103
|
+
]);
|
|
3104
|
+
const handleValidate = useCallback(({ property }) => {
|
|
3105
|
+
if (property && projection.getFields().find((f) => f.path === property && f.path !== field.path)) return [{
|
|
3106
|
+
path: "property",
|
|
3107
|
+
message: `property is not unique: '${property}'`
|
|
3108
|
+
}];
|
|
3109
|
+
}, [projection.getFields(), field]);
|
|
3110
|
+
const handleSave = useCallback((props) => {
|
|
3111
|
+
if (view) Obj.update(view, () => {
|
|
3112
|
+
projection.setFieldProjection({
|
|
3113
|
+
field,
|
|
3114
|
+
props
|
|
3115
|
+
});
|
|
3116
|
+
});
|
|
3117
|
+
else projection.setFieldProjection({
|
|
3118
|
+
field,
|
|
3119
|
+
props
|
|
3120
|
+
});
|
|
3121
|
+
onSave();
|
|
3122
|
+
}, [
|
|
3123
|
+
projection,
|
|
3124
|
+
field,
|
|
3125
|
+
view,
|
|
3126
|
+
onSave
|
|
3127
|
+
]);
|
|
3128
|
+
const handleCancel = useCallback(() => {
|
|
3129
|
+
onSave();
|
|
3130
|
+
requestAnimationFrame(() => onCancel?.());
|
|
3131
|
+
}, [onSave]);
|
|
3132
|
+
if (!fieldSchema) {
|
|
3133
|
+
log.warn("invalid format", { props }, {
|
|
3134
|
+
"~LogMeta": "~LogMeta",
|
|
3135
|
+
F: __dxlog_file$3,
|
|
3136
|
+
L: 185,
|
|
3137
|
+
S: void 0
|
|
3138
|
+
});
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
return /* @__PURE__ */ jsx(Form.Root, {
|
|
3142
|
+
fieldMap,
|
|
3143
|
+
autoFocus: true,
|
|
3144
|
+
readonly,
|
|
3145
|
+
schema: fieldSchema,
|
|
3146
|
+
values: props,
|
|
3147
|
+
filter: propIsNotType,
|
|
3148
|
+
sort: ["property", "format"],
|
|
3149
|
+
onValuesChanged: handleValuesChanged,
|
|
3150
|
+
onValidate: handleValidate,
|
|
3151
|
+
onSave: handleSave,
|
|
3152
|
+
onCancel: handleCancel,
|
|
3153
|
+
children: /* @__PURE__ */ jsxs(Form.Content, { children: [/* @__PURE__ */ jsx(Form.FieldSet, {}), /* @__PURE__ */ jsx(Form.Actions, {})] })
|
|
3154
|
+
}, field.id);
|
|
3155
|
+
};
|
|
3156
|
+
//#endregion
|
|
3157
|
+
//#region src/components/ObjectForm/ObjectForm.tsx
|
|
3158
|
+
var __dxlog_file$2 = "/__w/dxos/dxos/packages/ui/react-ui-form/src/components/ObjectForm/ObjectForm.tsx";
|
|
3159
|
+
var ObjectForm = ({ object, type, schema, showTags = true }) => {
|
|
3160
|
+
const db = Obj.getDatabase(object);
|
|
3161
|
+
const [snapshot] = useObject(object);
|
|
3162
|
+
const tags = [...Obj.getMeta(object).tags];
|
|
3163
|
+
const values = useMemo(() => ({
|
|
3164
|
+
[META_TAGS_KEY]: tags,
|
|
3165
|
+
...snapshot
|
|
3166
|
+
}), [snapshot, tags]);
|
|
3167
|
+
const formSchema = useMemo(() => {
|
|
3168
|
+
const base = schema ?? Type.getSchema(type);
|
|
3169
|
+
return showTags ? withMetaTags(base) : omitId(base);
|
|
3170
|
+
}, [
|
|
3171
|
+
schema,
|
|
3172
|
+
type,
|
|
3173
|
+
showTags
|
|
3174
|
+
]);
|
|
3175
|
+
const handleCreate = useCallback((type, values) => {
|
|
3176
|
+
invariant(db, void 0, {
|
|
3177
|
+
"~LogMeta": "~LogMeta",
|
|
3178
|
+
F: __dxlog_file$2,
|
|
3179
|
+
L: 49,
|
|
3180
|
+
S: void 0,
|
|
3181
|
+
A: ["db", ""]
|
|
3182
|
+
});
|
|
3183
|
+
invariant(Type.isObject(type), void 0, {
|
|
3184
|
+
"~LogMeta": "~LogMeta",
|
|
3185
|
+
F: __dxlog_file$2,
|
|
3186
|
+
L: 50,
|
|
3187
|
+
S: void 0,
|
|
3188
|
+
A: ["Type.isObject(type)", ""]
|
|
3189
|
+
});
|
|
3190
|
+
const newObject = db.add(Obj.make(type, values));
|
|
3191
|
+
if (Obj.instanceOf(Tag.Tag, newObject)) Obj.update(object, (object) => {
|
|
3192
|
+
Obj.getMeta(object).tags = [...Obj.getMeta(object).tags, Ref.make(newObject)];
|
|
3193
|
+
});
|
|
3194
|
+
}, []);
|
|
3195
|
+
const handleChange = useCallback(({ [META_TAGS_KEY]: metaTags, ...values }, { isValid, changed }) => {
|
|
3196
|
+
if (!isValid) return;
|
|
3197
|
+
const changedPaths = Object.keys(changed).filter((path) => changed[path]);
|
|
3198
|
+
if (changedPaths.some((path) => SchemaEx.splitJsonPath(path)[0] === "_tags")) Obj.update(object, (object) => {
|
|
3199
|
+
Obj.getMeta(object).tags = Array.isArray(metaTags) ? [...metaTags] : [];
|
|
3200
|
+
});
|
|
3201
|
+
const nonTagPaths = changedPaths.filter((path) => SchemaEx.splitJsonPath(path)[0] !== META_TAGS_KEY);
|
|
3202
|
+
if (nonTagPaths.length > 0) Obj.update(object, () => {
|
|
3203
|
+
for (const path of nonTagPaths) {
|
|
3204
|
+
const parts = SchemaEx.splitJsonPath(path);
|
|
3205
|
+
const value = Obj.getValue(values, parts);
|
|
3206
|
+
Obj.setValue(object, parts, value);
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
}, [object]);
|
|
3210
|
+
return /* @__PURE__ */ jsx(Form.Root, {
|
|
3211
|
+
schema: formSchema,
|
|
3212
|
+
values,
|
|
3213
|
+
createTypename: Type.getTypename(Tag.Tag),
|
|
3214
|
+
createOptionIcon: "ph--plus--regular",
|
|
3215
|
+
createOptionLabel: ["add-tag.label", { ns: translationKey }],
|
|
3216
|
+
createInitialValuePath: "label",
|
|
3217
|
+
createFieldMap: createFieldMap$1,
|
|
3218
|
+
db,
|
|
3219
|
+
onValuesChanged: handleChange,
|
|
3220
|
+
onCreate: handleCreate,
|
|
3221
|
+
children: /* @__PURE__ */ jsx(Form.Viewport, { children: /* @__PURE__ */ jsx(Form.Content, { children: /* @__PURE__ */ jsx(Form.FieldSet, {}) }) })
|
|
3222
|
+
});
|
|
3223
|
+
};
|
|
3224
|
+
var createFieldMap$1 = { hue: ({ type, label, presentation, getValue, onValueChange }) => {
|
|
3225
|
+
const handleChange = useCallback((nextHue) => onValueChange(type, nextHue), [onValueChange, type]);
|
|
3226
|
+
const handleReset = useCallback(() => onValueChange(type, void 0), [onValueChange, type]);
|
|
3227
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [presentation !== "inline" && /* @__PURE__ */ jsx(Form.Label, { label }), /* @__PURE__ */ jsx(HuePicker, {
|
|
3228
|
+
value: getValue(),
|
|
3229
|
+
onChange: handleChange,
|
|
3230
|
+
onReset: handleReset
|
|
3231
|
+
})] });
|
|
3232
|
+
} };
|
|
3233
|
+
//#endregion
|
|
3234
|
+
//#region src/components/ObjectProperties/ObjectProperties.tsx
|
|
3235
|
+
var __dxlog_file$1 = "/__w/dxos/dxos/packages/ui/react-ui-form/src/components/ObjectProperties/ObjectProperties.tsx";
|
|
3236
|
+
var ObjectProperties = composable(({ children, object, getCreateDefaults, resolveCreateEntry, ...props }, forwardedRef) => {
|
|
3237
|
+
const db = Obj.getDatabase(object);
|
|
3238
|
+
const tags = [...Obj.getMeta(object).tags];
|
|
3239
|
+
const values = useMemo(() => ({
|
|
3240
|
+
[META_TAGS_KEY]: tags,
|
|
3241
|
+
...object
|
|
3242
|
+
}), [object, tags]);
|
|
3243
|
+
const typeFromRegistry = useType(db, Obj.getTypeURI(object));
|
|
3244
|
+
const type = Obj.getType(object) ?? typeFromRegistry;
|
|
3245
|
+
const formSchema = useMemo(() => {
|
|
3246
|
+
return Function.pipe(type, Option.fromNullable, Option.map((type) => Type.getSchema(type)), Option.map((schema) => withMetaTags(schema)), Option.getOrUndefined);
|
|
3247
|
+
}, [type]);
|
|
3248
|
+
const handleCreate = useCallback((type, values) => {
|
|
3249
|
+
invariant(db, void 0, {
|
|
3250
|
+
"~LogMeta": "~LogMeta",
|
|
3251
|
+
F: __dxlog_file$1,
|
|
3252
|
+
L: 69,
|
|
3253
|
+
S: void 0,
|
|
3254
|
+
A: ["db", ""]
|
|
3255
|
+
});
|
|
3256
|
+
invariant(Type.isObject(type), void 0, {
|
|
3257
|
+
"~LogMeta": "~LogMeta",
|
|
3258
|
+
F: __dxlog_file$1,
|
|
3259
|
+
L: 70,
|
|
3260
|
+
S: void 0,
|
|
3261
|
+
A: ["Type.isObject(type)", ""]
|
|
3262
|
+
});
|
|
3263
|
+
const factory = Option.getOrUndefined(FactoryAnnotation.get(Type.getSchema(type)));
|
|
3264
|
+
const newObject = factory ? factory(values) : Obj.make(type, values);
|
|
3265
|
+
return db.add(newObject);
|
|
3266
|
+
}, [db]);
|
|
3267
|
+
const handleChange = useCallback(({ [META_TAGS_KEY]: metaTags, ...values }, { isValid, changed }) => {
|
|
3268
|
+
if (!isValid) return;
|
|
3269
|
+
const changedPaths = Object.keys(changed).filter((path) => changed[path]);
|
|
3270
|
+
if (changedPaths.some((path) => SchemaEx.splitJsonPath(path)[0] === "_tags")) Obj.update(object, (object) => {
|
|
3271
|
+
Obj.getMeta(object).tags = Array.isArray(metaTags) ? [...metaTags] : [];
|
|
3272
|
+
});
|
|
3273
|
+
const nonTagPaths = changedPaths.filter((path) => SchemaEx.splitJsonPath(path)[0] !== META_TAGS_KEY);
|
|
3274
|
+
if (nonTagPaths.length > 0) Obj.update(object, () => {
|
|
3275
|
+
for (const path of nonTagPaths) {
|
|
3276
|
+
const parts = SchemaEx.splitJsonPath(path);
|
|
3277
|
+
const value = Obj.getValue(values, parts);
|
|
3278
|
+
Obj.setValue(object, parts, value);
|
|
3279
|
+
}
|
|
3280
|
+
});
|
|
3281
|
+
}, [object]);
|
|
3282
|
+
if (!formSchema) return null;
|
|
3283
|
+
return /* @__PURE__ */ jsx(Form.Root, {
|
|
3284
|
+
db,
|
|
3285
|
+
schema: formSchema,
|
|
3286
|
+
defaultValues: values,
|
|
3287
|
+
createTypename: Type.getTypename(Tag.Tag),
|
|
3288
|
+
createOptionIcon: "ph--plus--regular",
|
|
3289
|
+
createOptionLabel: ["add-tag.label", { ns: translationKey }],
|
|
3290
|
+
createInitialValuePath: "label",
|
|
3291
|
+
createFieldMap,
|
|
3292
|
+
onValuesChanged: handleChange,
|
|
3293
|
+
onCreate: handleCreate,
|
|
3294
|
+
getCreateDefaults,
|
|
3295
|
+
resolveCreateEntry,
|
|
3296
|
+
children: /* @__PURE__ */ jsx(Form.Viewport, {
|
|
3297
|
+
...composableProps(props),
|
|
3298
|
+
scroll: true,
|
|
3299
|
+
ref: forwardedRef,
|
|
3300
|
+
children: /* @__PURE__ */ jsxs(Form.Content, { children: [/* @__PURE__ */ jsx(Form.FieldSet, {}), /* @__PURE__ */ jsx(Form.Section, { children })] })
|
|
3301
|
+
})
|
|
3302
|
+
});
|
|
3303
|
+
});
|
|
3304
|
+
var createFieldMap = { hue: ({ type, label, presentation, getValue, onValueChange }) => {
|
|
3305
|
+
const handleChange = useCallback((nextHue) => onValueChange(type, nextHue), [onValueChange, type]);
|
|
3306
|
+
const handleReset = useCallback(() => onValueChange(type, void 0), [onValueChange, type]);
|
|
3307
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [presentation !== "inline" && /* @__PURE__ */ jsx(Form.Label, { label }), /* @__PURE__ */ jsx(HuePicker, {
|
|
3308
|
+
value: getValue(),
|
|
3309
|
+
onChange: handleChange,
|
|
3310
|
+
onReset: handleReset
|
|
3311
|
+
})] });
|
|
3312
|
+
} };
|
|
3313
|
+
//#endregion
|
|
3314
|
+
//#region src/components/ObjectTree/ObjectTree.tsx
|
|
3315
|
+
/**
|
|
3316
|
+
* Render a value as a sequence of key/value rows whose shape and order are
|
|
3317
|
+
* driven by `schema`. Object-typed properties become a header row whose
|
|
3318
|
+
* children are indented under it; arrays expand element-by-element.
|
|
3319
|
+
*
|
|
3320
|
+
* Composable: spreads unknown props (incl. `className` / `classNames` /
|
|
3321
|
+
* `style` / `role`) onto its root `<div>` and forwards its ref, so it can
|
|
3322
|
+
* be slotted into themed parents like `Panel.Content`.
|
|
3323
|
+
*/
|
|
3324
|
+
var ObjectTreeImpl = composable(({ schema, value, ...props }, forwardedRef) => /* @__PURE__ */ jsx("div", {
|
|
3325
|
+
...composableProps(props, {
|
|
3326
|
+
role: "table",
|
|
3327
|
+
classNames: "font-mono text-sm w-full"
|
|
3328
|
+
}),
|
|
3329
|
+
ref: forwardedRef,
|
|
3330
|
+
children: /* @__PURE__ */ jsx(Node, {
|
|
3331
|
+
ast: schema.ast,
|
|
3332
|
+
value,
|
|
3333
|
+
label: null,
|
|
3334
|
+
depth: 0
|
|
3335
|
+
})
|
|
3336
|
+
}));
|
|
3337
|
+
ObjectTreeImpl.displayName = "ObjectTree";
|
|
3338
|
+
var ObjectTree = ObjectTreeImpl;
|
|
3339
|
+
/**
|
|
3340
|
+
* Peel wrappers that don't affect the runtime shape: Refinements pass their
|
|
3341
|
+
* `from`, Suspends resolve via `f()`, and Transformations expose the encoded
|
|
3342
|
+
* (input) side. Repeated unwrapping handles chains like `Schema.suspend ->
|
|
3343
|
+
* Refinement -> TypeLiteral`.
|
|
3344
|
+
*/
|
|
3345
|
+
var unwrap = (ast) => {
|
|
3346
|
+
if (SchemaAST.isRefinement(ast)) return unwrap(ast.from);
|
|
3347
|
+
if (SchemaAST.isSuspend(ast)) return unwrap(ast.f());
|
|
3348
|
+
if (SchemaAST.isTransformation(ast)) return unwrap(ast.from);
|
|
3349
|
+
return ast;
|
|
3350
|
+
};
|
|
3351
|
+
/**
|
|
3352
|
+
* Effect models `Schema.optional(X)` as a property whose type AST is
|
|
3353
|
+
* `Union(X, UndefinedKeyword)`. For walking purposes we want X -- the
|
|
3354
|
+
* undefined branch carries no schema information for the viewer.
|
|
3355
|
+
*/
|
|
3356
|
+
var stripOptional = (ast) => {
|
|
3357
|
+
if (!SchemaAST.isUnion(ast)) return ast;
|
|
3358
|
+
const nonUndef = ast.types.filter((t) => !SchemaAST.isUndefinedKeyword(t));
|
|
3359
|
+
return nonUndef.length === 1 ? nonUndef[0] : ast;
|
|
3360
|
+
};
|
|
3361
|
+
var Node = ({ ast, value, label, depth }) => {
|
|
3362
|
+
const inner = unwrap(stripOptional(ast));
|
|
3363
|
+
if (SchemaAST.isTypeLiteral(inner) && isPlainObject(value)) {
|
|
3364
|
+
const childDepth = label === null ? depth : depth + 1;
|
|
3365
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [label !== null && /* @__PURE__ */ jsx(Row, {
|
|
3366
|
+
label,
|
|
3367
|
+
value: null,
|
|
3368
|
+
depth,
|
|
3369
|
+
kind: "group"
|
|
3370
|
+
}), inner.propertySignatures.map((prop) => /* @__PURE__ */ jsx(Node, {
|
|
3371
|
+
ast: prop.type,
|
|
3372
|
+
value: value[String(prop.name)],
|
|
3373
|
+
label: String(prop.name),
|
|
3374
|
+
depth: childDepth
|
|
3375
|
+
}, String(prop.name)))] });
|
|
3376
|
+
}
|
|
3377
|
+
if (SchemaAST.isTupleType(inner) && Array.isArray(value)) {
|
|
3378
|
+
const elemType = inner.rest[0]?.type;
|
|
3379
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [label !== null && /* @__PURE__ */ jsx(Row, {
|
|
3380
|
+
label,
|
|
3381
|
+
value: `Array(${value.length})`,
|
|
3382
|
+
depth,
|
|
3383
|
+
kind: "group"
|
|
3384
|
+
}), value.map((item, index) => /* @__PURE__ */ jsx(Node, {
|
|
3385
|
+
ast: elemType ?? SchemaAST.unknownKeyword,
|
|
3386
|
+
value: item,
|
|
3387
|
+
label: `[${index}]`,
|
|
3388
|
+
depth: depth + 1
|
|
3389
|
+
}, index))] });
|
|
3390
|
+
}
|
|
3391
|
+
return /* @__PURE__ */ jsx(Row, {
|
|
3392
|
+
label: label ?? "",
|
|
3393
|
+
value: formatLeaf(value),
|
|
3394
|
+
depth,
|
|
3395
|
+
kind: "leaf"
|
|
3396
|
+
});
|
|
3397
|
+
};
|
|
3398
|
+
var Row = ({ label, value, depth }) => /* @__PURE__ */ jsxs("div", {
|
|
3399
|
+
role: "row",
|
|
3400
|
+
className: mx("flex items-baseline gap-3 py-1"),
|
|
3401
|
+
style: { paddingInlineStart: 8 + depth * 16 },
|
|
3402
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3403
|
+
role: "cell",
|
|
3404
|
+
className: "text-subdued",
|
|
3405
|
+
children: label
|
|
3406
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
3407
|
+
role: "cell",
|
|
3408
|
+
className: mx("break-all text-base-fg"),
|
|
3409
|
+
children: value
|
|
3410
|
+
})]
|
|
3411
|
+
});
|
|
3412
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3413
|
+
/**
|
|
3414
|
+
* Format a leaf value for display. Strings are quoted so empty strings are
|
|
3415
|
+
* visible; missing values surface as an em-dash so the row stays present.
|
|
3416
|
+
*/
|
|
3417
|
+
var formatLeaf = (value) => {
|
|
3418
|
+
if (value === void 0) return null;
|
|
3419
|
+
if (value === null) return /* @__PURE__ */ jsx("span", {
|
|
3420
|
+
className: "text-description",
|
|
3421
|
+
children: "null"
|
|
3422
|
+
});
|
|
3423
|
+
switch (typeof value) {
|
|
3424
|
+
case "string": return /* @__PURE__ */ jsx("span", {
|
|
3425
|
+
className: "text-blue-text",
|
|
3426
|
+
children: JSON.stringify(value)
|
|
3427
|
+
});
|
|
3428
|
+
case "number":
|
|
3429
|
+
case "bigint": return /* @__PURE__ */ jsx("span", {
|
|
3430
|
+
className: "text-green-text",
|
|
3431
|
+
children: String(value)
|
|
3432
|
+
});
|
|
3433
|
+
case "boolean": return /* @__PURE__ */ jsx("span", {
|
|
3434
|
+
className: "text-purple-text",
|
|
3435
|
+
children: String(value)
|
|
3436
|
+
});
|
|
3437
|
+
default: try {
|
|
3438
|
+
return JSON.stringify(value);
|
|
3439
|
+
} catch {
|
|
3440
|
+
return String(value);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
};
|
|
3444
|
+
//#endregion
|
|
3445
|
+
//#region src/components/RefEditor/ref-editor-extension.ts
|
|
3446
|
+
/** Matches an object reference token (`@<object id>`). */
|
|
3447
|
+
var REF_REGEX = /^@([A-Za-z0-9]+)$/;
|
|
3448
|
+
/** Matches a well-formed email address token. */
|
|
3449
|
+
var EMAIL_REGEX = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
|
3450
|
+
/** Matches an RFC 5322 name-addr (`Name <email>`); group 1 = display name, group 2 = addr-spec. */
|
|
3451
|
+
var NAME_ADDR_REGEX = /^(.*?)\s*<([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>$/;
|
|
3452
|
+
/** Formats an RFC 5322 mailbox (`Name <email>`). */
|
|
3453
|
+
var formatMailbox = (name, email) => name ? `${name} <${email}>` : email;
|
|
3454
|
+
/**
|
|
3455
|
+
* Effect dispatched when reference data changes out-of-band (e.g. objects finish loading) so that
|
|
3456
|
+
* already-rendered reference decorations re-resolve without a document change.
|
|
3457
|
+
*/
|
|
3458
|
+
var refEditorRedecorate = StateEffect.define();
|
|
3459
|
+
/**
|
|
3460
|
+
* CodeMirror extension rendering reference-editor tokens as tags. Tags are atomic: the cursor
|
|
3461
|
+
* cannot enter them and deletion removes the whole token.
|
|
3462
|
+
* - `'ref'` mode: `@<id>` references resolve to the object's label (via
|
|
3463
|
+
* {@link RefEditorExtensionOptions.getRef}); tokens accepted by the pluggable `match` regexp
|
|
3464
|
+
* render as neutral tags.
|
|
3465
|
+
* - `'email'` mode: RFC 5322 mailboxes render as tags labelled with the display name (falling
|
|
3466
|
+
* back to the address); the trailing comma separator is hidden inside the tag's atomic range.
|
|
3467
|
+
*/
|
|
3468
|
+
var refEditor = ({ mode = "ref", getRef, match, colorize } = {}) => {
|
|
3469
|
+
let decoField;
|
|
3470
|
+
/** Delete the tag whose delete affordance (`node`) was clicked, keeping the delimited list well-formed. */
|
|
3471
|
+
const removeToken = (view, node) => {
|
|
3472
|
+
const index = [...view.contentDOM.querySelectorAll("button[aria-label=\"Remove\"]")].indexOf(node);
|
|
3473
|
+
if (index < 0) return;
|
|
3474
|
+
const cursor = view.state.field(decoField).iter();
|
|
3475
|
+
let range;
|
|
3476
|
+
for (let current = 0; cursor.value; current++, cursor.next()) if (current === index) {
|
|
3477
|
+
range = {
|
|
3478
|
+
from: cursor.from,
|
|
3479
|
+
to: cursor.to
|
|
3480
|
+
};
|
|
3481
|
+
break;
|
|
3482
|
+
}
|
|
3483
|
+
if (!range) return;
|
|
3484
|
+
const doc = view.state.doc.toString();
|
|
3485
|
+
let { from } = range;
|
|
3486
|
+
const { to } = range;
|
|
3487
|
+
if (mode === "email" && !/,\s*$/.test(doc.slice(from, to))) {
|
|
3488
|
+
const dangling = doc.slice(0, from).match(/,\s*$/);
|
|
3489
|
+
if (dangling) from -= dangling[0].length;
|
|
3490
|
+
}
|
|
3491
|
+
view.dispatch({
|
|
3492
|
+
changes: {
|
|
3493
|
+
from,
|
|
3494
|
+
to
|
|
3495
|
+
},
|
|
3496
|
+
selection: { anchor: from }
|
|
3497
|
+
});
|
|
3498
|
+
};
|
|
3499
|
+
const buildRefDecorations = (state) => {
|
|
3500
|
+
const deco = new RangeSetBuilder();
|
|
3501
|
+
const text = state.doc.toString();
|
|
3502
|
+
const tokenRegex = /\S+/g;
|
|
3503
|
+
let tokenMatch;
|
|
3504
|
+
while (tokenMatch = tokenRegex.exec(text)) {
|
|
3505
|
+
const token = tokenMatch[0];
|
|
3506
|
+
const from = tokenMatch.index;
|
|
3507
|
+
const to = from + token.length;
|
|
3508
|
+
const refMatch = token.match(REF_REGEX);
|
|
3509
|
+
if (refMatch) {
|
|
3510
|
+
const ref = getRef?.(refMatch[1]);
|
|
3511
|
+
if (ref) deco.add(from, to, Decoration.replace({ widget: new RefWidget(ref.label, ref.hue ?? (colorize ? getHashHue(refMatch[1]) : void 0), removeToken) }));
|
|
3512
|
+
continue;
|
|
3513
|
+
}
|
|
3514
|
+
if (match?.test(token)) deco.add(from, to, Decoration.replace({ widget: new RefWidget(token, void 0, removeToken) }));
|
|
3515
|
+
}
|
|
3516
|
+
return deco.finish();
|
|
3517
|
+
};
|
|
3518
|
+
const buildEmailDecorations = (state) => {
|
|
3519
|
+
const addrRegex = match ?? EMAIL_REGEX;
|
|
3520
|
+
const deco = new RangeSetBuilder();
|
|
3521
|
+
const text = state.doc.toString();
|
|
3522
|
+
const segmentRegex = /[^,]+(?:,\s*)?|,\s*/g;
|
|
3523
|
+
let segmentMatch;
|
|
3524
|
+
while (segmentMatch = segmentRegex.exec(text)) {
|
|
3525
|
+
const segment = segmentMatch[0];
|
|
3526
|
+
const commaIndex = segment.indexOf(",");
|
|
3527
|
+
const content = (commaIndex === -1 ? segment : segment.slice(0, commaIndex)).trim();
|
|
3528
|
+
if (!content) continue;
|
|
3529
|
+
if (commaIndex === -1) continue;
|
|
3530
|
+
const leading = segment.length - segment.trimStart().length;
|
|
3531
|
+
const from = segmentMatch.index + leading;
|
|
3532
|
+
const to = segmentMatch.index + segment.length;
|
|
3533
|
+
const nameAddr = content.match(NAME_ADDR_REGEX);
|
|
3534
|
+
if (nameAddr) deco.add(from, to, Decoration.replace({ widget: new RefWidget(nameAddr[1] || nameAddr[2], colorize ? getHashHue(nameAddr[2]) : void 0, removeToken) }));
|
|
3535
|
+
else if (addrRegex.test(content)) deco.add(from, to, Decoration.replace({ widget: new RefWidget(content, void 0, removeToken) }));
|
|
3536
|
+
}
|
|
3537
|
+
return deco.finish();
|
|
3538
|
+
};
|
|
3539
|
+
const buildDecorations = mode === "email" ? buildEmailDecorations : buildRefDecorations;
|
|
3540
|
+
decoField = StateField.define({
|
|
3541
|
+
create: (state) => buildDecorations(state),
|
|
3542
|
+
update: (deco, tr) => {
|
|
3543
|
+
if (tr.docChanged || tr.effects.some((effect) => effect.is(refEditorRedecorate))) return buildDecorations(tr.state);
|
|
3544
|
+
return deco;
|
|
3545
|
+
},
|
|
3546
|
+
provide: (field) => [EditorView.decorations.from(field), EditorView.atomicRanges.of((view) => view.state.field(field))]
|
|
3547
|
+
});
|
|
3548
|
+
const separator = mode === "email" ? ", " : " ";
|
|
3549
|
+
const separatorFilter = EditorState.transactionFilter.of((tr) => {
|
|
3550
|
+
if (!tr.docChanged || !tr.isUserEvent("input")) return tr;
|
|
3551
|
+
const tags = tr.startState.field(decoField, false);
|
|
3552
|
+
if (!tags) return tr;
|
|
3553
|
+
let extra;
|
|
3554
|
+
tr.changes.iterChanges((fromA, toA, _fromB, toB, inserted) => {
|
|
3555
|
+
if (extra || fromA !== toA) return;
|
|
3556
|
+
const text = inserted.toString();
|
|
3557
|
+
if (!text || /[\s,]$/.test(text)) return;
|
|
3558
|
+
let abutsTag = false;
|
|
3559
|
+
for (const cursor = tags.iter(); cursor.value; cursor.next()) {
|
|
3560
|
+
if (cursor.from === fromA) {
|
|
3561
|
+
abutsTag = true;
|
|
3562
|
+
break;
|
|
3563
|
+
}
|
|
3564
|
+
if (cursor.from > fromA) break;
|
|
3565
|
+
}
|
|
3566
|
+
if (abutsTag) extra = {
|
|
3567
|
+
changes: {
|
|
3568
|
+
from: toB,
|
|
3569
|
+
insert: separator
|
|
3570
|
+
},
|
|
3571
|
+
sequential: true,
|
|
3572
|
+
selection: { anchor: toB }
|
|
3573
|
+
};
|
|
3574
|
+
});
|
|
3575
|
+
return extra ? [tr, extra] : tr;
|
|
3576
|
+
});
|
|
3577
|
+
return [
|
|
3578
|
+
styles,
|
|
3579
|
+
decoField,
|
|
3580
|
+
separatorFilter
|
|
3581
|
+
];
|
|
3582
|
+
};
|
|
3583
|
+
/**
|
|
3584
|
+
* The outer span vertically aligns the tag with the surrounding line and adds a trailing gap so
|
|
3585
|
+
* adjacent tags don't touch. The inner span carries the border; `overflow-hidden` clips the child
|
|
3586
|
+
* fills to its radius (a square-cornered fill otherwise peeks past the rounded border).
|
|
3587
|
+
*/
|
|
3588
|
+
var container = (classNames, ...children) => {
|
|
3589
|
+
const inner = Domino.of("span").classNames(mx("inline-flex items-stretch border rounded-xs overflow-hidden", classNames)).append(...children);
|
|
3590
|
+
return Domino.of("span").classNames("inline-flex align-middle pe-1").append(inner).root;
|
|
3591
|
+
};
|
|
3592
|
+
/** Trailing `x` affordance that deletes the tag it belongs to. */
|
|
3593
|
+
var deleteButton = (view, remove) => {
|
|
3594
|
+
const button = Domino.of("button").classNames("flex items-center px-1 text-description hover:text-error cursor-pointer").attributes({
|
|
3595
|
+
"type": "button",
|
|
3596
|
+
"aria-label": "Remove"
|
|
3597
|
+
}).append(Domino.svg("ph--x--bold").classNames("shrink-0 w-3.5 h-3.5")).on("mousedown", (event) => {
|
|
3598
|
+
event.preventDefault();
|
|
3599
|
+
remove(view, button.root);
|
|
3600
|
+
});
|
|
3601
|
+
return button;
|
|
3602
|
+
};
|
|
3603
|
+
/**
|
|
3604
|
+
* Resolved reference (object reference or RFC 5322 mailbox), rendered as the object's label with a
|
|
3605
|
+
* trailing delete affordance.
|
|
3606
|
+
*/
|
|
3607
|
+
var RefWidget = class extends WidgetType {
|
|
3608
|
+
_label;
|
|
3609
|
+
_hue;
|
|
3610
|
+
_remove;
|
|
3611
|
+
constructor(_label, _hue, _remove) {
|
|
3612
|
+
super();
|
|
3613
|
+
this._label = _label;
|
|
3614
|
+
this._hue = _hue;
|
|
3615
|
+
this._remove = _remove;
|
|
3616
|
+
}
|
|
3617
|
+
eq(other) {
|
|
3618
|
+
return this._label === other._label && this._hue === other._hue;
|
|
3619
|
+
}
|
|
3620
|
+
toDOM(view) {
|
|
3621
|
+
return container(mx("border-separator rounded-sm", (this._hue ? getStyles(this._hue) : void 0)?.border), Domino.of("span").classNames("flex items-center ps-2 pe-1").text(this._label), deleteButton(view, this._remove));
|
|
3622
|
+
}
|
|
3623
|
+
};
|
|
3624
|
+
var styles = EditorView.theme({ ".cm-line": {
|
|
3625
|
+
padding: "0",
|
|
3626
|
+
display: "flex",
|
|
3627
|
+
alignItems: "center"
|
|
3628
|
+
} });
|
|
3629
|
+
//#endregion
|
|
3630
|
+
//#region src/components/RefEditor/RefEditor.tsx
|
|
3631
|
+
/**
|
|
3632
|
+
* Single-line input for a list of object references. Typing `@` (or any text, with
|
|
3633
|
+
* `activateOnTyping`) opens a typeahead menu matching objects of the given type by label (and
|
|
3634
|
+
* by their `getValues` values). The content model depends on {@link RefEditorProps.mode}.
|
|
3635
|
+
* Resolved entries render as atomic tags with the object's label; raw tokens accepted by the
|
|
3636
|
+
* pluggable `match` regexp render as neutral tags.
|
|
3637
|
+
*/
|
|
3638
|
+
var RefEditor = forwardRef(({ db, type, value, readonly, mode = "ref", match, getLabel, getValues, icon = "ph--link--regular", placeholder: placeholderProp, activateOnTyping, numItems = 8, classNames, ...props }, forwardedRef) => {
|
|
3639
|
+
const { t } = useTranslation(translationKey);
|
|
3640
|
+
const { themeMode } = useThemeContext();
|
|
3641
|
+
const getObjectLabel = useCallback((object) => getLabel?.(object) ?? Obj.getLabel(object) ?? object.id, [getLabel]);
|
|
3642
|
+
const objects = useQuery(db, useMemo(() => Filter.type(type), [type]));
|
|
3643
|
+
const refs = useMemo(() => new Map(objects.map((object) => [object.id, {
|
|
3644
|
+
label: getObjectLabel(object),
|
|
3645
|
+
hue: getHashHue(object.id)
|
|
3646
|
+
}])), [objects, getObjectLabel]);
|
|
3647
|
+
const refsRef = useRef(refs);
|
|
3648
|
+
refsRef.current = refs;
|
|
3649
|
+
const controllerRef = useRef(null);
|
|
3650
|
+
const composedRef = useCallback((controller) => {
|
|
3651
|
+
controllerRef.current = controller;
|
|
3652
|
+
if (typeof forwardedRef === "function") forwardedRef(controller);
|
|
3653
|
+
else if (forwardedRef) forwardedRef.current = controller;
|
|
3654
|
+
}, [forwardedRef]);
|
|
3655
|
+
useEffect(() => {
|
|
3656
|
+
controllerRef.current?.view?.dispatch({ effects: refEditorRedecorate.of(null) });
|
|
3657
|
+
}, [refs]);
|
|
3658
|
+
const getMenu = useCallback(async ({ text }) => {
|
|
3659
|
+
if (!text) return [];
|
|
3660
|
+
const query = text.replace(/^@/, "").toLowerCase();
|
|
3661
|
+
const matchesQuery = (object) => !query || getObjectLabel(object).toLowerCase().includes(query) || (getValues?.(object) ?? []).some((entry) => entry.toLowerCase().includes(query));
|
|
3662
|
+
if (mode === "email") return [{
|
|
3663
|
+
id: "mailboxes",
|
|
3664
|
+
items: objects.flatMap((object) => (getValues?.(object) ?? []).map((entry) => ({
|
|
3665
|
+
object,
|
|
3666
|
+
entry
|
|
3667
|
+
}))).filter(({ object, entry }) => !query || getObjectLabel(object).toLowerCase().includes(query) || entry.toLowerCase().includes(query)).slice(0, numItems * 2).map(({ object, entry }) => ({
|
|
3668
|
+
id: `${object.id}-${entry}`,
|
|
3669
|
+
label: `${getObjectLabel(object)} (${entry})`,
|
|
3670
|
+
icon,
|
|
3671
|
+
onSelect: ({ view, head }) => insertMailbox(view, head, formatMailbox(getObjectLabel(object), entry))
|
|
3672
|
+
}))
|
|
3673
|
+
}];
|
|
3674
|
+
return [{
|
|
3675
|
+
id: "refs",
|
|
3676
|
+
items: objects.filter(matchesQuery).slice(0, numItems * 2).map((object) => {
|
|
3677
|
+
const label = getObjectLabel(object);
|
|
3678
|
+
const detail = getValues?.(object)?.[0];
|
|
3679
|
+
return {
|
|
3680
|
+
id: object.id,
|
|
3681
|
+
label: detail && detail !== label ? `${label} (${detail})` : label,
|
|
3682
|
+
icon,
|
|
3683
|
+
onSelect: ({ view, head }) => insertAtCursor(view, head, `@${object.id} `)
|
|
3684
|
+
};
|
|
3685
|
+
})
|
|
3686
|
+
}];
|
|
3687
|
+
}, [
|
|
3688
|
+
objects,
|
|
3689
|
+
numItems,
|
|
3690
|
+
mode,
|
|
3691
|
+
icon,
|
|
3692
|
+
getObjectLabel,
|
|
3693
|
+
getValues
|
|
3694
|
+
]);
|
|
3695
|
+
const placeholder = useMemo(() => ({
|
|
3696
|
+
content: placeholderProp ?? t(activateOnTyping ? "ref-editor-auto.placeholder" : "ref-editor.placeholder"),
|
|
3697
|
+
focusOnly: false
|
|
3698
|
+
}), [
|
|
3699
|
+
t,
|
|
3700
|
+
placeholderProp,
|
|
3701
|
+
activateOnTyping
|
|
3702
|
+
]);
|
|
3703
|
+
const extensions = useMemo(() => [
|
|
3704
|
+
createBasicExtensions({
|
|
3705
|
+
readOnly: readonly,
|
|
3706
|
+
lineWrapping: false
|
|
3707
|
+
}),
|
|
3708
|
+
createThemeExtensions({
|
|
3709
|
+
themeMode,
|
|
3710
|
+
slots: {
|
|
3711
|
+
editor: { className: "w-full" },
|
|
3712
|
+
scroller: { className: "scrollbar-none" }
|
|
3713
|
+
}
|
|
3714
|
+
}),
|
|
3715
|
+
refEditor({
|
|
3716
|
+
mode,
|
|
3717
|
+
match,
|
|
3718
|
+
getRef: (id) => refsRef.current.get(id)
|
|
3719
|
+
}),
|
|
3720
|
+
Prec.highest(keymap.of([{
|
|
3721
|
+
key: "Enter",
|
|
3722
|
+
run: (view) => {
|
|
3723
|
+
const { head } = view.state.selection.main;
|
|
3724
|
+
const line = view.state.doc.lineAt(head);
|
|
3725
|
+
const before = line.text.slice(0, head - line.from);
|
|
3726
|
+
if (mode === "email") {
|
|
3727
|
+
const segment = before.slice(before.lastIndexOf(",") + 1).trim();
|
|
3728
|
+
if (NAME_ADDR_REGEX.test(segment) || (match ?? EMAIL_REGEX).test(segment)) insertAtCursor(view, head, ", ");
|
|
3729
|
+
} else {
|
|
3730
|
+
const token = before.slice(before.search(/\S+$/));
|
|
3731
|
+
if (token && match?.test(token)) insertAtCursor(view, head, " ");
|
|
3732
|
+
}
|
|
3733
|
+
return true;
|
|
3734
|
+
}
|
|
3735
|
+
}, {
|
|
3736
|
+
key: "Space",
|
|
3737
|
+
run: (view) => {
|
|
3738
|
+
if (mode !== "email") return false;
|
|
3739
|
+
const { head } = view.state.selection.main;
|
|
3740
|
+
const line = view.state.doc.lineAt(head);
|
|
3741
|
+
const before = line.text.slice(0, head - line.from);
|
|
3742
|
+
const segment = before.slice(before.lastIndexOf(",") + 1).trim();
|
|
3743
|
+
if ((match ?? EMAIL_REGEX).test(segment)) {
|
|
3744
|
+
insertAtCursor(view, head, ", ");
|
|
3745
|
+
return true;
|
|
3746
|
+
}
|
|
3747
|
+
return false;
|
|
3748
|
+
}
|
|
3749
|
+
}]))
|
|
3750
|
+
], [
|
|
3751
|
+
readonly,
|
|
3752
|
+
themeMode,
|
|
3753
|
+
mode,
|
|
3754
|
+
match
|
|
3755
|
+
]);
|
|
3756
|
+
return /* @__PURE__ */ jsx(Editor.Root, {
|
|
3757
|
+
extensions,
|
|
3758
|
+
numItems,
|
|
3759
|
+
trigger: "@",
|
|
3760
|
+
activateOnTyping,
|
|
3761
|
+
placeholder,
|
|
3762
|
+
filter: false,
|
|
3763
|
+
getMenu,
|
|
3764
|
+
ref: composedRef,
|
|
3765
|
+
children: /* @__PURE__ */ jsx(Editor.View, {
|
|
3766
|
+
...props,
|
|
3767
|
+
classNames: mx("flex items-center dx-input h-[2rem]", classNames),
|
|
3768
|
+
initialValue: value,
|
|
3769
|
+
selectionEnd: true
|
|
3770
|
+
})
|
|
3771
|
+
});
|
|
3772
|
+
});
|
|
3773
|
+
RefEditor.displayName = "RefEditor";
|
|
3774
|
+
/**
|
|
3775
|
+
* Insert an RFC 5322 mailbox, replacing the in-progress segment (anything typed after the last
|
|
3776
|
+
* comma — multi-word queries can leave words outside the popover's completion range).
|
|
3777
|
+
*/
|
|
3778
|
+
var insertMailbox = (view, head, mailbox) => {
|
|
3779
|
+
const line = view.state.doc.lineAt(head);
|
|
3780
|
+
const lastComma = line.text.slice(0, head - line.from).lastIndexOf(",");
|
|
3781
|
+
const from = line.from + (lastComma === -1 ? 0 : lastComma + 1);
|
|
3782
|
+
const insert = `${lastComma === -1 ? "" : " "}${mailbox}, `;
|
|
3783
|
+
view.dispatch({
|
|
3784
|
+
changes: {
|
|
3785
|
+
from,
|
|
3786
|
+
to: head,
|
|
3787
|
+
insert
|
|
3788
|
+
},
|
|
3789
|
+
selection: {
|
|
3790
|
+
anchor: from + insert.length,
|
|
3791
|
+
head: from + insert.length
|
|
3792
|
+
}
|
|
3793
|
+
});
|
|
3794
|
+
};
|
|
3795
|
+
//#endregion
|
|
3796
|
+
//#region src/components/ViewEditor/ViewEditor.tsx
|
|
3797
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-form/src/components/ViewEditor/ViewEditor.tsx";
|
|
3798
|
+
/**
|
|
3799
|
+
* View editor form.
|
|
3800
|
+
*/
|
|
3801
|
+
var ViewEditor = forwardRef(({ classNames, type, view, mode = "schema", registry, db, readonly, showHeading = false, types, tags, onQueryChanged, onDelete }, forwardedRef) => {
|
|
3802
|
+
const atomRegistry = useContext(RegistryContext);
|
|
3803
|
+
const schemaReadonly = type == null || Type.getDatabase(type) == null;
|
|
3804
|
+
const { t } = useTranslation(translationKey);
|
|
3805
|
+
const projectionModel = useMemo(() => {
|
|
3806
|
+
if (!type) return null;
|
|
3807
|
+
const jsonSchema = type.jsonSchema;
|
|
3808
|
+
const change = createEchoChangeCallback(view, Type.getDatabase(type) != null ? type : void 0);
|
|
3809
|
+
return new ProjectionModel({
|
|
3810
|
+
registry: atomRegistry,
|
|
3811
|
+
view,
|
|
3812
|
+
baseSchema: jsonSchema,
|
|
3813
|
+
change
|
|
3814
|
+
});
|
|
3815
|
+
}, [
|
|
3816
|
+
atomRegistry,
|
|
3817
|
+
type,
|
|
3818
|
+
view
|
|
3819
|
+
]);
|
|
3820
|
+
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
3821
|
+
const feedTarget = Match.value(view.query.ast).pipe(Match.when({ type: "from" }, ({ from }) => {
|
|
3822
|
+
if (from._tag !== "scope") return;
|
|
3823
|
+
const feedScope = from.scopes.find((s) => s._tag === "feed");
|
|
3824
|
+
return Option.fromNullable(feedScope).pipe(Option.map((s) => s.feedUri), Option.getOrUndefined);
|
|
3825
|
+
}), Match.orElse(() => void 0));
|
|
3826
|
+
const feeds = useQuery(db, Filter.type(Feed.Feed));
|
|
3827
|
+
const targetRef = useMemo(() => {
|
|
3828
|
+
if (!feedTarget) return;
|
|
3829
|
+
const targetEid = EID.tryParse(feedTarget);
|
|
3830
|
+
const feed = feeds.find((feed) => {
|
|
3831
|
+
const feedEid = Feed.getFeedUri(feed);
|
|
3832
|
+
return feedEid != null && targetEid != null && EID.equals(feedEid, targetEid);
|
|
3833
|
+
});
|
|
3834
|
+
return feed ? Ref.fromURI(Entity.getURI(feed)) : void 0;
|
|
3835
|
+
}, [feedTarget, feeds]);
|
|
3836
|
+
const viewSchema = useMemo(() => {
|
|
3837
|
+
const base = Schema.Struct({ query: mode === "schema" ? Format.URL.annotations({ title: "Record type" }) : QueryAST.Query.annotations({ title: "Query" }) });
|
|
3838
|
+
if (mode === "tag") return Schema.Struct({
|
|
3839
|
+
...base.fields,
|
|
3840
|
+
target: Ref.Ref(Feed.Feed).pipe(Schema.annotations({ title: "Target Feed" }), ParentLabelAnnotation.set(true), Schema.optional)
|
|
3841
|
+
}).pipe(Schema.mutable);
|
|
3842
|
+
return base.pipe(Schema.mutable);
|
|
3843
|
+
}, [mode]);
|
|
3844
|
+
const viewValues = useMemo(() => {
|
|
3845
|
+
const typeUri = getTypeURIFromQuery(view.query.ast);
|
|
3846
|
+
const typeDxn = typeUri ? DXN.tryMake(typeUri) : void 0;
|
|
3847
|
+
return {
|
|
3848
|
+
query: mode === "schema" ? typeDxn ? DXN.getName(typeDxn) : typeUri ?? "" : view.query.ast,
|
|
3849
|
+
target: targetRef
|
|
3850
|
+
};
|
|
3851
|
+
}, [
|
|
3852
|
+
mode,
|
|
3853
|
+
view.query.ast,
|
|
3854
|
+
targetRef
|
|
3855
|
+
]);
|
|
3856
|
+
const fieldMap = useMemo(() => mode === "tag" ? customFields({
|
|
3857
|
+
types,
|
|
3858
|
+
tags
|
|
3859
|
+
}) : void 0, [
|
|
3860
|
+
mode,
|
|
3861
|
+
types,
|
|
3862
|
+
tags
|
|
3863
|
+
]);
|
|
3864
|
+
const handleUpdate = useCallback((values) => {
|
|
3865
|
+
const targetValue = values.target;
|
|
3866
|
+
let feedDxn;
|
|
3867
|
+
if (Ref.isRef(targetValue)) {
|
|
3868
|
+
const feedUri = targetValue.uri;
|
|
3869
|
+
const feed = feeds.find((feed) => Obj.getURI(feed) === feedUri);
|
|
3870
|
+
if (feed) feedDxn = Feed.getFeedUri(feed);
|
|
3871
|
+
}
|
|
3872
|
+
const query = mode === "schema" ? Query.select(Filter.type(EID.isEID(values.query) ? values.query : DXN.make(values.query))).ast : JSON.parse(JSON.stringify(values.query));
|
|
3873
|
+
onQueryChanged?.(query, feedDxn);
|
|
3874
|
+
}, [
|
|
3875
|
+
onQueryChanged,
|
|
3876
|
+
mode,
|
|
3877
|
+
feeds
|
|
3878
|
+
]);
|
|
3879
|
+
const handleDelete = useCallback((fieldId) => {
|
|
3880
|
+
invariant(!readonly, void 0, {
|
|
3881
|
+
"~LogMeta": "~LogMeta",
|
|
3882
|
+
F: __dxlog_file,
|
|
3883
|
+
L: 207,
|
|
3884
|
+
S: void 0,
|
|
3885
|
+
A: ["!readonly", ""]
|
|
3886
|
+
});
|
|
3887
|
+
onDelete?.(fieldId);
|
|
3888
|
+
}, [onDelete, readonly]);
|
|
3889
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3890
|
+
className: mx(classNames),
|
|
3891
|
+
children: /* @__PURE__ */ jsx(Form.Root, {
|
|
3892
|
+
schema: viewSchema,
|
|
3893
|
+
values: viewValues,
|
|
3894
|
+
fieldMap,
|
|
3895
|
+
db,
|
|
3896
|
+
onValuesChanged: handleUpdate,
|
|
3897
|
+
children: /* @__PURE__ */ jsx(Form.Viewport, { children: /* @__PURE__ */ jsxs(Form.Content, { children: [
|
|
3898
|
+
schemaReadonly && !readonly && /* @__PURE__ */ jsx(Message.Root, {
|
|
3899
|
+
valence: "info",
|
|
3900
|
+
classNames: "my-form-padding",
|
|
3901
|
+
children: /* @__PURE__ */ jsx(Message.Title, { children: t("system-schema.description") })
|
|
3902
|
+
}),
|
|
3903
|
+
/* @__PURE__ */ jsx(Form.FieldSet, {}),
|
|
3904
|
+
type && /* @__PURE__ */ jsx(FieldList, {
|
|
3905
|
+
type,
|
|
3906
|
+
view,
|
|
3907
|
+
registry,
|
|
3908
|
+
readonly,
|
|
3909
|
+
showHeading,
|
|
3910
|
+
onDelete: handleDelete
|
|
3911
|
+
})
|
|
3912
|
+
] }) })
|
|
3913
|
+
})
|
|
3914
|
+
});
|
|
3915
|
+
});
|
|
3916
|
+
var customFields = ({ types, tags }) => ({ query: ({ type, readonly, label, getValue, onValueChange }) => {
|
|
3917
|
+
const handleChange = useCallback((query) => onValueChange(type, query.ast), [onValueChange]);
|
|
3918
|
+
return /* @__PURE__ */ jsxs(Input.Root, { children: [/* @__PURE__ */ jsx(FormFieldLabel, {
|
|
3919
|
+
readonly,
|
|
3920
|
+
label
|
|
3921
|
+
}), /* @__PURE__ */ jsx(QueryForm, {
|
|
3922
|
+
initialQuery: getValue(),
|
|
3923
|
+
types,
|
|
3924
|
+
tags,
|
|
3925
|
+
onChange: handleChange
|
|
3926
|
+
})] });
|
|
3927
|
+
} });
|
|
3928
|
+
var FieldList = ({ type, view, registry, readonly, showHeading = false, onDelete }) => {
|
|
3929
|
+
const atomRegistry = useContext(RegistryContext);
|
|
3930
|
+
const schemaReadonly = Type.getDatabase(type) == null;
|
|
3931
|
+
const { t } = useTranslation(translationKey);
|
|
3932
|
+
const [viewSnapshot] = useObject(view);
|
|
3933
|
+
const projectionModel = useMemo(() => {
|
|
3934
|
+
const jsonSchema = type.jsonSchema;
|
|
3935
|
+
const change = createEchoChangeCallback(view, Type.getDatabase(type) != null ? type : void 0);
|
|
3936
|
+
return new ProjectionModel({
|
|
3937
|
+
registry: atomRegistry,
|
|
3938
|
+
view,
|
|
3939
|
+
baseSchema: jsonSchema,
|
|
3940
|
+
change
|
|
3941
|
+
});
|
|
3942
|
+
}, [
|
|
3943
|
+
atomRegistry,
|
|
3944
|
+
type,
|
|
3945
|
+
view
|
|
3946
|
+
]);
|
|
3947
|
+
const [expandedField, setExpandedField] = useState();
|
|
3948
|
+
const handleAdd = useCallback(() => {
|
|
3949
|
+
invariant(!readonly, void 0, {
|
|
3950
|
+
"~LogMeta": "~LogMeta",
|
|
3951
|
+
F: __dxlog_file,
|
|
3952
|
+
L: 290,
|
|
3953
|
+
S: void 0,
|
|
3954
|
+
A: ["!readonly", ""]
|
|
3955
|
+
});
|
|
3956
|
+
const field = projectionModel.createFieldProjection();
|
|
3957
|
+
setExpandedField(field.id);
|
|
3958
|
+
}, [projectionModel, readonly]);
|
|
3959
|
+
const handleDelete = useCallback((fieldId) => {
|
|
3960
|
+
invariant(!readonly, void 0, {
|
|
3961
|
+
"~LogMeta": "~LogMeta",
|
|
3962
|
+
F: __dxlog_file,
|
|
3963
|
+
L: 297,
|
|
3964
|
+
S: void 0,
|
|
3965
|
+
A: ["!readonly", ""]
|
|
3966
|
+
});
|
|
3967
|
+
if (fieldId === expandedField) setExpandedField(void 0);
|
|
3968
|
+
onDelete?.(fieldId);
|
|
3969
|
+
}, [
|
|
3970
|
+
expandedField,
|
|
3971
|
+
onDelete,
|
|
3972
|
+
readonly
|
|
3973
|
+
]);
|
|
3974
|
+
const handleMove = useCallback((fromIndex, toIndex) => {
|
|
3975
|
+
invariant(!readonly, void 0, {
|
|
3976
|
+
"~LogMeta": "~LogMeta",
|
|
3977
|
+
F: __dxlog_file,
|
|
3978
|
+
L: 309,
|
|
3979
|
+
S: void 0,
|
|
3980
|
+
A: ["!readonly", ""]
|
|
3981
|
+
});
|
|
3982
|
+
Obj.update(view, (view) => {
|
|
3983
|
+
const fields = [...view.projection.fields];
|
|
3984
|
+
const [moved] = fields.splice(fromIndex, 1);
|
|
3985
|
+
fields.splice(toIndex, 0, moved);
|
|
3986
|
+
view.projection.fields = fields;
|
|
3987
|
+
});
|
|
3988
|
+
}, [view, readonly]);
|
|
3989
|
+
const handleClose = useCallback(() => setExpandedField(void 0), []);
|
|
3990
|
+
const handleHide = useCallback((fieldId) => {
|
|
3991
|
+
setExpandedField(void 0);
|
|
3992
|
+
projectionModel.hideFieldProjection(fieldId);
|
|
3993
|
+
}, [projectionModel]);
|
|
3994
|
+
const handleShow = useCallback((property) => {
|
|
3995
|
+
setExpandedField(void 0);
|
|
3996
|
+
projectionModel.showFieldProjection(property);
|
|
3997
|
+
}, [projectionModel]);
|
|
3998
|
+
if (!viewSnapshot) return null;
|
|
3999
|
+
return /* @__PURE__ */ jsx(OrderedList.Root, {
|
|
4000
|
+
items: viewSnapshot.projection.fields,
|
|
4001
|
+
isItem: Schema.is(View.FieldSchema),
|
|
4002
|
+
getId: (field) => field.id,
|
|
4003
|
+
onMove: readonly ? void 0 : handleMove,
|
|
4004
|
+
readonly,
|
|
4005
|
+
expandedId: expandedField,
|
|
4006
|
+
onExpandedChange: setExpandedField,
|
|
4007
|
+
children: ({ items: fields }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4008
|
+
/* @__PURE__ */ jsx(FormFieldHeader, {
|
|
4009
|
+
label: t("fields.label"),
|
|
4010
|
+
readonly,
|
|
4011
|
+
add: {
|
|
4012
|
+
label: t("add-property-button.label"),
|
|
4013
|
+
disabled: viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT,
|
|
4014
|
+
onClick: handleAdd
|
|
4015
|
+
}
|
|
4016
|
+
}),
|
|
4017
|
+
showHeading && /* @__PURE__ */ jsx(FormFieldLabel, {
|
|
4018
|
+
standalone: true,
|
|
4019
|
+
classNames: "py-1",
|
|
4020
|
+
label: t("field-path.label")
|
|
4021
|
+
}),
|
|
4022
|
+
/* @__PURE__ */ jsx(OrderedList.Content, { children: fields.map((field) => {
|
|
4023
|
+
const hidden = field.visible === false;
|
|
4024
|
+
return /* @__PURE__ */ jsx(OrderedList.DetailItem, {
|
|
4025
|
+
id: field.id,
|
|
4026
|
+
item: field,
|
|
4027
|
+
canDrag: !readonly && !schemaReadonly,
|
|
4028
|
+
expandable: !readonly,
|
|
4029
|
+
title: field.path,
|
|
4030
|
+
titleClassNames: hidden ? "text-subdued" : void 0,
|
|
4031
|
+
actions: /* @__PURE__ */ jsx(ToggleIconButton, {
|
|
4032
|
+
iconOnly: true,
|
|
4033
|
+
variant: "ghost",
|
|
4034
|
+
active: hidden,
|
|
4035
|
+
icon: "ph--eye--regular",
|
|
4036
|
+
activeIcon: "ph--eye-closed--regular",
|
|
4037
|
+
label: t(hidden ? "show-field.label" : "hide-field.label"),
|
|
4038
|
+
"data-testid": hidden ? "show-field-button" : "hide-field-button",
|
|
4039
|
+
disabled: readonly || !hidden && projectionModel.getFields().length <= 1,
|
|
4040
|
+
onClick: () => hidden ? handleShow(field.path) : handleHide(field.id)
|
|
4041
|
+
}),
|
|
4042
|
+
trailing: !readonly && /* @__PURE__ */ jsx(CompactIconButton, {
|
|
4043
|
+
icon: "ph--x--regular",
|
|
4044
|
+
label: t("delete-field.label"),
|
|
4045
|
+
disabled: schemaReadonly || viewSnapshot.projection.fields.length <= 1,
|
|
4046
|
+
onClick: () => handleDelete(field.id),
|
|
4047
|
+
"data-testid": "field.delete"
|
|
4048
|
+
}),
|
|
4049
|
+
children: /* @__PURE__ */ jsx(FieldEditor, {
|
|
4050
|
+
readonly: readonly || schemaReadonly,
|
|
4051
|
+
registry,
|
|
4052
|
+
projection: projectionModel,
|
|
4053
|
+
field,
|
|
4054
|
+
onSave: handleClose
|
|
4055
|
+
})
|
|
4056
|
+
}, field.id);
|
|
4057
|
+
}) })
|
|
4058
|
+
] })
|
|
4059
|
+
});
|
|
4060
|
+
};
|
|
4061
|
+
//#endregion
|
|
4062
|
+
export { ArrayField, AsyncSelectField, AutofillAnnotation, AutofillAnnotationId, AutofillField, BooleanField, ComboboxField, CompactIconButton, DateField, EMAIL_REGEX, FieldEditor, Form, FormContextProvider, FormField, FormFieldErrorBoundary, FormFieldHeader, FormFieldLabel, FormFieldSet, FormFieldSetContainer, FormRow, GeoPointField, InlineRefField, META_TAGS_KEY, MarkdownField, NAME_ADDR_REGEX, NumberField, ObjectForm, ObjectPicker, ObjectProperties, ObjectTree, OptionsLookupAnnotation, OptionsLookupAnnotationId, PasswordField, REF_REGEX, RefEditor, RefField, SelectField, SelectOptionField, TextAreaField, TextField, TupleField, ViewEditor, autofill, cellClassesForFieldType, createSelectField, findRefOption, formatMailbox, getDefaultValue, getFormProperties, getNumericConstraints, getRefProps, getRootFormProperties, omitHiddenFormFields, omitId, optionsLookup, parseValue, pathNotUniqueError, pickValues, presentationFor, refEditor, refEditorRedecorate, useAsyncFieldEffect, useFormContext, useFormFieldState, useFormHandler, useFormValues, useKeyHandler, withMetaTags };
|
|
4063
|
+
|
|
4064
|
+
//# sourceMappingURL=index.mjs.map
|