@qoretechnologies/reqraft 0.10.53 → 0.10.54-pr.115.g9bc96ed
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/.claude/CLAUDE.md +25 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
- package/README.md +1 -1
- package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
- package/design/FORM_ENGINE_FIELD_MIGRATION.md +56 -1
- package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
- package/design/SMART_EDITOR_UX.md +21 -0
- package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
- package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
- package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
- package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlTags.js +5 -1
- package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
- package/dist/components/dpqlEditor/index.d.ts +4 -0
- package/dist/components/dpqlEditor/index.d.ts.map +1 -1
- package/dist/components/dpqlEditor/index.js +5 -1
- package/dist/components/dpqlEditor/index.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +42 -2
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/dpqlEditor/types.js +0 -4
- package/dist/components/dpqlEditor/types.js.map +1 -1
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
- package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
- package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
- package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +192 -45
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +5 -2
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +160 -12
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +376 -75
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
- package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +61 -11
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +42 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +74 -7
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +13 -0
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
- package/dist/components/form/engine/rendererTypes.js.map +1 -1
- package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
- package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
- package/dist/components/form/engine/rowMenuContext.js +103 -0
- package/dist/components/form/engine/rowMenuContext.js.map +1 -0
- package/dist/components/form/engine/typeChoices.d.ts +37 -0
- package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
- package/dist/components/form/engine/typeChoices.js +25 -0
- package/dist/components/form/engine/typeChoices.js.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.js +53 -0
- package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
- package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +202 -45
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
- package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
- package/dist/components/form/expressions/argumentPresence.js +203 -0
- package/dist/components/form/expressions/argumentPresence.js.map +1 -0
- package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
- package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts +15 -2
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +20 -58
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
- package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
- package/dist/components/form/expressions/dpqlMockLanguage.d.ts +76 -0
- package/dist/components/form/expressions/dpqlMockLanguage.d.ts.map +1 -0
- package/dist/components/form/expressions/dpqlMockLanguage.js +525 -0
- package/dist/components/form/expressions/dpqlMockLanguage.js.map +1 -0
- package/dist/components/form/expressions/dpqlMockLsp.d.ts +31 -2
- package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
- package/dist/components/form/expressions/dpqlMockLsp.js +86 -80
- package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +8 -4
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +95 -19
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/types.d.ts +7 -0
- package/dist/components/form/expressions/types.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +50 -126
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +3 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +32 -8
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/date/Date.d.ts +1 -1
- package/dist/components/form/fields/date/Date.d.ts.map +1 -1
- package/dist/components/form/fields/date/Date.js +16 -2
- package/dist/components/form/fields/date/Date.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +21 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +140 -8
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +31 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +528 -50
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
- package/dist/components/smartEditor/SmartEditor.js +29 -8
- package/dist/components/smartEditor/SmartEditor.js.map +1 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +8 -0
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js +53 -10
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +5 -1
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +16 -2
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/helpers/dpqlDetection.d.ts +58 -0
- package/dist/helpers/dpqlDetection.d.ts.map +1 -0
- package/dist/helpers/dpqlDetection.js +164 -0
- package/dist/helpers/dpqlDetection.js.map +1 -0
- package/dist/helpers/optionUiTypes.d.ts +3 -3
- package/dist/helpers/options.d.ts +37 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +78 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/templateItems.d.ts +47 -0
- package/dist/helpers/templateItems.d.ts.map +1 -0
- package/dist/helpers/templateItems.js +88 -0
- package/dist/helpers/templateItems.js.map +1 -0
- package/dist/helpers/templateText.d.ts +37 -0
- package/dist/helpers/templateText.d.ts.map +1 -0
- package/dist/helpers/templateText.js +64 -0
- package/dist/helpers/templateText.js.map +1 -0
- package/dist/helpers/templates.d.ts +19 -11
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +46 -29
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +56 -9
- package/dist/helpers/validations.js.map +1 -1
- package/dist/hooks/useArgSchema.d.ts +2 -1
- package/dist/hooks/useArgSchema.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.d.ts +3 -1
- package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.js +47 -9
- package/dist/hooks/useFetch/useFetch.js.map +1 -1
- package/dist/hooks/useTemplates.d.ts +20 -0
- package/dist/hooks/useTemplates.d.ts.map +1 -1
- package/dist/hooks/useTemplates.js +34 -2
- package/dist/hooks/useTemplates.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/stories/storyNetwork.d.ts +36 -0
- package/dist/stories/storyNetwork.d.ts.map +1 -0
- package/dist/stories/storyNetwork.js +82 -0
- package/dist/stories/storyNetwork.js.map +1 -0
- package/dist/utils/fetch.d.ts +151 -1
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +256 -44
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/lspClient.d.ts +6 -0
- package/dist/utils/lspClient.d.ts.map +1 -1
- package/dist/utils/lspClient.js +62 -11
- package/dist/utils/lspClient.js.map +1 -1
- package/dist/utils/websocket.d.ts +6 -0
- package/dist/utils/websocket.d.ts.map +1 -1
- package/dist/utils/websocket.js +15 -7
- package/dist/utils/websocket.js.map +1 -1
- package/package.json +2 -2
- package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
- package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
- package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
- package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
- package/src/components/dpqlEditor/dpqlTags.ts +13 -0
- package/src/components/dpqlEditor/index.ts +4 -0
- package/src/components/dpqlEditor/types.ts +38 -2
- package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
- package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
- package/src/components/form/engine/CompactRow.tsx +240 -51
- package/src/components/form/engine/CompactToolbar.tsx +10 -3
- package/src/components/form/engine/FormEngine.stories.tsx +423 -120
- package/src/components/form/engine/FormEngine.tsx +504 -87
- package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +89 -11
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
- package/src/components/form/engine/compactToolbarContext.ts +1 -1
- package/src/components/form/engine/readFirst.ts +86 -7
- package/src/components/form/engine/rendererTypes.ts +12 -0
- package/src/components/form/engine/rowMenuContext.ts +159 -0
- package/src/components/form/engine/typeChoices.ts +55 -0
- package/src/components/form/expressions/DpqlRendering.tsx +85 -0
- package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
- package/src/components/form/expressions/ExpressionField.tsx +265 -54
- package/src/components/form/expressions/argumentPresence.ts +200 -0
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +20 -4
- package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
- package/src/components/form/expressions/builder/index.tsx +39 -77
- package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
- package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
- package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
- package/src/components/form/expressions/mockExpressions.ts +8 -4
- package/src/components/form/expressions/renderExpressionToText.ts +112 -22
- package/src/components/form/expressions/types.ts +7 -0
- package/src/components/form/expressions/useRenderExpression.ts +43 -127
- package/src/components/form/fields/Field.tsx +3 -2
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +27 -20
- package/src/components/form/fields/auto/AutoFormField.tsx +33 -9
- package/src/components/form/fields/date/Date.tsx +17 -1
- package/src/components/form/fields/rich-text/RichText.tsx +199 -22
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
- package/src/components/form/fields/template/TemplateField.stories.tsx +31 -9
- package/src/components/form/fields/template/TemplateField.tsx +714 -122
- package/src/components/form/index.tsx +1 -0
- package/src/components/log/Log.stories.tsx +2 -1
- package/src/components/smartEditor/SmartEditor.tsx +41 -9
- package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
- package/src/components/smartEditor/useLspSession.ts +21 -3
- package/src/helpers/dpqlDetection.ts +209 -0
- package/src/helpers/options.ts +70 -0
- package/src/helpers/templateItems.ts +81 -0
- package/src/helpers/templateText.ts +92 -0
- package/src/helpers/templates.ts +47 -37
- package/src/helpers/validations.ts +59 -9
- package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
- package/src/hooks/useFetch/useFetch.tsx +53 -7
- package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
- package/src/hooks/useTemplates.ts +41 -0
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
- package/src/index.tsx +14 -1
- package/src/stores/currentUser/currentUser.stories.tsx +2 -1
- package/src/stories/storyNetwork.ts +92 -0
- package/src/utils/fetch.ts +311 -21
- package/src/utils/lspClient.ts +68 -11
- package/src/utils/websocket.ts +20 -9
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// Owns the `IExpression` value and offers two views of the same AST —
|
|
4
4
|
// Visual (the builder, Phase 3) and Text (DPQL, this phase). The DPQL text
|
|
5
5
|
// editor bridges to the AST via the LSP `dpql/parse` (text → AST on edit)
|
|
6
|
-
// and `dpql/serialize` (AST → text on entering Text mode); a
|
|
7
|
-
//
|
|
6
|
+
// and `dpql/serialize` (AST → text on entering Text mode); a debounced
|
|
7
|
+
// "Preview" box renders the current AST (`dpql/renderExpression`) when that
|
|
8
|
+
// rendering differs from the text the author typed.
|
|
8
9
|
import {
|
|
9
10
|
ReqoreButton,
|
|
10
11
|
ReqoreControlGroup,
|
|
@@ -13,7 +14,9 @@ import {
|
|
|
13
14
|
import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
14
15
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
16
|
import { DpqlEditor, IDpqlEditorRef } from '../../dpqlEditor';
|
|
17
|
+
import { dpqlDisplayedText } from '../../dpqlEditor/dpqlHelpers';
|
|
16
18
|
import { ExpressionBuilder, IExpressionBuilderProps } from './builder';
|
|
19
|
+
import { DpqlRendering } from './DpqlRendering';
|
|
17
20
|
import { IExpression, IExpressionSchema, IExpressionValue, TExpressionReorder } from './types';
|
|
18
21
|
import { useExpressions } from './useExpressions';
|
|
19
22
|
import { useRenderExpression } from './useRenderExpression';
|
|
@@ -21,12 +24,19 @@ import { useRenderExpression } from './useRenderExpression';
|
|
|
21
24
|
export type TExpressionMode = 'visual' | 'text';
|
|
22
25
|
|
|
23
26
|
const PARSE_DEBOUNCE_MS = 300;
|
|
27
|
+
/**
|
|
28
|
+
* How long the Preview waits before catching up with the editor.
|
|
29
|
+
*
|
|
30
|
+
* It mirrors the AST, so without this it re-renders on every keystroke — a box
|
|
31
|
+
* appearing, changing and vanishing under the line being typed. Slightly
|
|
32
|
+
* SLOWER than the parse it follows, so the preview settles once after the text
|
|
33
|
+
* has stopped moving rather than twitching on the way there.
|
|
34
|
+
*/
|
|
35
|
+
const PREVIEW_DEBOUNCE_MS = 400;
|
|
24
36
|
/** Retry cadence for seeding the Text editor while its LSP session opens. */
|
|
25
37
|
const SEED_RETRY_MS = 400;
|
|
26
38
|
const SEED_MAX_TRIES = 20;
|
|
27
39
|
|
|
28
|
-
const noop = (): void => undefined;
|
|
29
|
-
|
|
30
40
|
export interface IExpressionFieldProps {
|
|
31
41
|
/** The expression value: `{ is_expression:true, value:{ exp, args } }`. */
|
|
32
42
|
value?: IExpression;
|
|
@@ -62,6 +72,12 @@ export interface IExpressionFieldProps {
|
|
|
62
72
|
recordType?: string;
|
|
63
73
|
/** Initial editor mode (default `visual`). */
|
|
64
74
|
defaultMode?: TExpressionMode;
|
|
75
|
+
/**
|
|
76
|
+
* SEAM (reqraft): the host's per-`ui_type` editors, forwarded to the
|
|
77
|
+
* builder's operand fields. Without them an operand typed with one of the
|
|
78
|
+
* CONSUMER's ui_types renders "Unknown type!".
|
|
79
|
+
*/
|
|
80
|
+
componentOverrides?: Record<string, React.FC<any>>;
|
|
65
81
|
size?: string;
|
|
66
82
|
/** Forwarded to the builder — operand reordering for varargs expressions. */
|
|
67
83
|
reorder?: TExpressionReorder;
|
|
@@ -83,6 +99,7 @@ export const ExpressionField = memo(
|
|
|
83
99
|
recordType,
|
|
84
100
|
defaultMode = 'visual',
|
|
85
101
|
size,
|
|
102
|
+
componentOverrides,
|
|
86
103
|
reorder,
|
|
87
104
|
}: IExpressionFieldProps) => {
|
|
88
105
|
const { expressions } = useExpressions({
|
|
@@ -92,68 +109,218 @@ export const ExpressionField = memo(
|
|
|
92
109
|
const { renderRich } = useRenderExpression();
|
|
93
110
|
|
|
94
111
|
const [mode, setMode] = useState<TExpressionMode>(defaultMode);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
112
|
+
/* The server's rendering of the current AST; empty until it has one. There is
|
|
113
|
+
no client-side stand-in — see `useRenderExpression`. */
|
|
114
|
+
const [preview, setPreview] = useState('');
|
|
99
115
|
|
|
100
116
|
// Text mode state. `text` is the DPQL string the editor shows; the AST
|
|
101
117
|
// (`value`) stays the source of truth, kept in sync via parse-on-edit.
|
|
102
118
|
const [text, setText] = useState('');
|
|
119
|
+
/* The text the current AST was parsed FROM.
|
|
120
|
+
*
|
|
121
|
+
* The preview renders the AST, and the AST only moves on a SUCCESSFUL
|
|
122
|
+
* parse — so while an edit is mid-flight or simply invalid, the AST still
|
|
123
|
+
* describes the last thing that parsed. Showing it then states something
|
|
124
|
+
* the editor does not contain: deleting the `2` from `1 + 2` left
|
|
125
|
+
* "Unexpected end of input" sitting directly above a Preview confidently
|
|
126
|
+
* reading `1 + 2`. */
|
|
127
|
+
const [astText, setAstText] = useState('');
|
|
128
|
+
/**
|
|
129
|
+
* The server's answer to "can this expression's result satisfy the type
|
|
130
|
+
* this field declares?". `null` when nothing has been asked, or when the
|
|
131
|
+
* field declares no type worth asking about.
|
|
132
|
+
*/
|
|
133
|
+
const [typeCheck, setTypeCheck] = useState<{
|
|
134
|
+
compatible: boolean;
|
|
135
|
+
mayFail: boolean;
|
|
136
|
+
inferred?: string;
|
|
137
|
+
target?: string;
|
|
138
|
+
fix?: string;
|
|
139
|
+
} | null>(null);
|
|
103
140
|
const dpqlRef = useRef<IDpqlEditorRef>(null);
|
|
104
141
|
const parseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
105
142
|
|
|
106
143
|
const ast = useMemo<IExpressionValue | undefined>(() => value?.value, [value]);
|
|
107
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The type the expression's result has to satisfy, sent with every parse.
|
|
147
|
+
*
|
|
148
|
+
* `auto` and `any` are left out deliberately: they accept anything, so the
|
|
149
|
+
* server would answer "compatible" for everything and the round trip would
|
|
150
|
+
* buy nothing. A field offering a CHOICE of return types has no single
|
|
151
|
+
* answer either, so it asks nothing rather than asking about the wrong one.
|
|
152
|
+
*/
|
|
153
|
+
const targetType = useMemo<string | undefined>(() => {
|
|
154
|
+
const declared = Array.isArray(returnType) ? undefined : (returnType ?? type);
|
|
155
|
+
return declared && declared !== 'auto' && declared !== 'any' ? declared : undefined;
|
|
156
|
+
}, [returnType, type]);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Read the type analysis off a parse result, or clear it.
|
|
160
|
+
*
|
|
161
|
+
* "Fits" is read from the whole answer, not `type_compatible` alone. A
|
|
162
|
+
* conversion that exists and cannot fail — a number used as text — is a
|
|
163
|
+
* fit as far as the author is concerned, and reporting it is exactly the
|
|
164
|
+
* noise the message below is written to avoid.
|
|
165
|
+
*/
|
|
166
|
+
const readTypeCheck = useCallback(
|
|
167
|
+
(result?: { type_compatible?: boolean; auto_coercible?: boolean;
|
|
168
|
+
coercion_may_fail?: boolean; inferred_type?: string; target_type?: string;
|
|
169
|
+
suggested_fix?: { text: string } }): void => {
|
|
170
|
+
if (!result || result.type_compatible === undefined) {
|
|
171
|
+
setTypeCheck(null);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const convertsSafely = !!result.auto_coercible && !result.coercion_may_fail;
|
|
175
|
+
setTypeCheck({
|
|
176
|
+
compatible: !!result.type_compatible || convertsSafely,
|
|
177
|
+
mayFail: !!result.coercion_may_fail,
|
|
178
|
+
inferred: result.inferred_type,
|
|
179
|
+
target: result.target_type,
|
|
180
|
+
fix: result.suggested_fix?.text,
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
[]
|
|
184
|
+
);
|
|
185
|
+
|
|
108
186
|
// Keep a readable rendering of the current expression in sync — the
|
|
109
|
-
// single live mirror of the AST (server `dpql/renderExpression`
|
|
110
|
-
//
|
|
187
|
+
// single live mirror of the AST (the server's `dpql/renderExpression`).
|
|
188
|
+
//
|
|
189
|
+
// Debounced: it follows the AST, which follows the text, so rendering it
|
|
190
|
+
// eagerly put a box under the cursor that appeared, changed and vanished
|
|
191
|
+
// while the author was still typing.
|
|
111
192
|
useEffect(() => {
|
|
112
193
|
let live = true;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
194
|
+
const timer = setTimeout(() => {
|
|
195
|
+
renderRich(ast ?? {}).then((r) => {
|
|
196
|
+
if (live) setPreview(r?.text ?? '');
|
|
197
|
+
});
|
|
198
|
+
}, PREVIEW_DEBOUNCE_MS);
|
|
116
199
|
return () => {
|
|
117
200
|
live = false;
|
|
201
|
+
clearTimeout(timer);
|
|
118
202
|
};
|
|
119
|
-
}, [renderRich, ast
|
|
203
|
+
}, [renderRich, ast]);
|
|
120
204
|
|
|
121
205
|
// Tracks whether the user has typed since entering Text mode, so a
|
|
122
206
|
// slow seed response can't clobber their input.
|
|
123
207
|
const userTypedRef = useRef(false);
|
|
124
|
-
|
|
208
|
+
|
|
209
|
+
/* A cleared value must clear the editor with it.
|
|
210
|
+
*
|
|
211
|
+
* "Clear value" empties the option (`handleValueChange(name, undefined)`),
|
|
212
|
+
* but the DPQL text is local state seeded once on entering Text mode, and
|
|
213
|
+
* the seeding effect returns early for an empty AST — it exists to fill the
|
|
214
|
+
* editor, not to empty it. So the value went and the text stayed: the row
|
|
215
|
+
* reported "This field is required" while still showing `1 + 2`, which
|
|
216
|
+
* reads as the clear having silently failed.
|
|
217
|
+
*
|
|
218
|
+
* Only an AST that HAD content and lost it clears the box. A field that
|
|
219
|
+
* simply starts empty is left alone, so this cannot wipe what an author is
|
|
220
|
+
* part-way through typing before their first parse lands.
|
|
221
|
+
*/
|
|
222
|
+
const hadExpression = useRef(false);
|
|
125
223
|
useEffect(() => {
|
|
126
|
-
|
|
224
|
+
/* GONE, not merely incomplete.
|
|
225
|
+
*
|
|
226
|
+
* The first cut asked whether the AST still had an `exp`, and that is a
|
|
227
|
+
* state the value passes THROUGH while the author types: a half-written
|
|
228
|
+
* expression parses to something without one. So typing `1 + 2` cleared
|
|
229
|
+
* the editor mid-keystroke and the text vanished under the cursor.
|
|
230
|
+
*
|
|
231
|
+
* A clear removes the value itself, so that is what to watch: `value`
|
|
232
|
+
* empty, not an AST that is between shapes. Typing never produces that —
|
|
233
|
+
* `handleDpqlChange` only emits on a SUCCESSFUL parse, so an incomplete
|
|
234
|
+
* expression leaves the previous value in place. */
|
|
235
|
+
if (ast) {
|
|
236
|
+
hadExpression.current = true;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (hadExpression.current) {
|
|
240
|
+
hadExpression.current = false;
|
|
241
|
+
userTypedRef.current = false;
|
|
242
|
+
setText('');
|
|
243
|
+
}
|
|
127
244
|
}, [ast]);
|
|
128
245
|
|
|
129
246
|
// Text mode: parse the DPQL into the AST (debounced).
|
|
247
|
+
/* The session may not be attached when the debounce fires — the editor
|
|
248
|
+
mounts on the render that flips the mode, and its LSP session comes up
|
|
249
|
+
after that. `dpqlRef.current?.parse?.()` is optional all the way down, so
|
|
250
|
+
an early call resolves `undefined`, `readTypeCheck` CLEARS the analysis,
|
|
251
|
+
and nothing asks again: the author types once into a fresh Text field and
|
|
252
|
+
is told nothing about the type until they happen to type another
|
|
253
|
+
character.
|
|
254
|
+
|
|
255
|
+
The seeding effect below already retries for this exact reason
|
|
256
|
+
("`serialize` resolves '' until the editor's LSP session is ready"); the
|
|
257
|
+
parse path needed the same care and did not have it. Same budget as
|
|
258
|
+
seeding, so a session that never arrives gives up rather than spinning.
|
|
259
|
+
|
|
260
|
+
Found by CI, not locally: the session is up before the debounce on a
|
|
261
|
+
fast machine, so the story asserting the type message passed here every
|
|
262
|
+
time and failed on the runner. */
|
|
130
263
|
const handleDpqlChange = useCallback(
|
|
131
264
|
(next: string) => {
|
|
132
265
|
userTypedRef.current = true;
|
|
133
266
|
setText(next);
|
|
134
267
|
if (parseTimer.current) clearTimeout(parseTimer.current);
|
|
135
|
-
|
|
136
|
-
|
|
268
|
+
let tries = 0;
|
|
269
|
+
const runParse = async (): Promise<void> => {
|
|
270
|
+
const parse = dpqlRef.current?.parse;
|
|
271
|
+
if (!parse) {
|
|
272
|
+
if (++tries < SEED_MAX_TRIES) {
|
|
273
|
+
parseTimer.current = setTimeout(runParse, SEED_RETRY_MS);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const result = await parse(next, targetType);
|
|
278
|
+
readTypeCheck(result);
|
|
137
279
|
if (result?.success && result.expression) {
|
|
280
|
+
setAstText(next);
|
|
138
281
|
// `dpql/parse` returns the field-ready `{ is_expression, value }`.
|
|
139
282
|
onChange(result.expression as IExpression);
|
|
140
283
|
}
|
|
141
|
-
}
|
|
284
|
+
};
|
|
285
|
+
parseTimer.current = setTimeout(runParse, PARSE_DEBOUNCE_MS);
|
|
142
286
|
},
|
|
143
|
-
[onChange]
|
|
287
|
+
[onChange, targetType, readTypeCheck]
|
|
144
288
|
);
|
|
145
289
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
290
|
+
/** Whether this visit to Text mode has already put the AST in the editor. */
|
|
291
|
+
const seededRef = useRef(false);
|
|
292
|
+
|
|
293
|
+
/* Seed the Text editor from the AST whenever Text mode becomes active
|
|
294
|
+
* (including a `defaultMode='text'` mount). This must run as an effect: the
|
|
295
|
+
* editor only mounts on the same render that flips the mode (so `dpqlRef`
|
|
296
|
+
* is null inside the click handler), and `serialize` resolves '' until the
|
|
297
|
+
* editor's LSP session is ready — hence the brief retry loop.
|
|
298
|
+
*
|
|
299
|
+
* It waits for an AST, because the AST can arrive AFTER the mode does. A
|
|
300
|
+
* host flips this shell into Text mode on the very render that accepts an
|
|
301
|
+
* expression, and the value it passes on that render is still the TEXT the
|
|
302
|
+
* author typed — the parsed AST only comes back through the form one render
|
|
303
|
+
* later. Keyed on the mode alone, the effect had already had its only turn
|
|
304
|
+
* by then: it found nothing to serialize, returned, and nothing asked
|
|
305
|
+
* again. The author landed in an empty editor with a Preview of their own
|
|
306
|
+
* expression sitting beside it, which reads as the text having been thrown
|
|
307
|
+
* away.
|
|
308
|
+
*
|
|
309
|
+
* Seeding happens at most ONCE per visit, and that is what makes watching
|
|
310
|
+
* the AST safe: `userTypedRef` guards what is being typed, but the AST
|
|
311
|
+
* MOVES as the author types — every successful parse replaces it — so an
|
|
312
|
+
* effect that re-seeded on each new AST would fight them for the editor. */
|
|
152
313
|
useEffect(() => {
|
|
153
|
-
if (mode !== 'text')
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
314
|
+
if (mode !== 'text') {
|
|
315
|
+
// Leaving Text asks the question again on the next visit; the typing
|
|
316
|
+
// flag goes with it, or a stale one would suppress that visit's seed.
|
|
317
|
+
seededRef.current = false;
|
|
318
|
+
userTypedRef.current = false;
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
if (seededRef.current || userTypedRef.current) return undefined;
|
|
322
|
+
if (!ast?.exp) return undefined;
|
|
323
|
+
const seedAst = ast;
|
|
157
324
|
let cancelled = false;
|
|
158
325
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
159
326
|
let tries = 0;
|
|
@@ -161,7 +328,9 @@ export const ExpressionField = memo(
|
|
|
161
328
|
const t = await dpqlRef.current?.serialize?.(seedAst);
|
|
162
329
|
if (cancelled || userTypedRef.current) return;
|
|
163
330
|
if (t) {
|
|
331
|
+
seededRef.current = true;
|
|
164
332
|
setText(t);
|
|
333
|
+
setAstText(t);
|
|
165
334
|
} else if (++tries < SEED_MAX_TRIES) {
|
|
166
335
|
timer = setTimeout(seed, SEED_RETRY_MS);
|
|
167
336
|
}
|
|
@@ -171,7 +340,7 @@ export const ExpressionField = memo(
|
|
|
171
340
|
cancelled = true;
|
|
172
341
|
if (timer) clearTimeout(timer);
|
|
173
342
|
};
|
|
174
|
-
}, [mode]);
|
|
343
|
+
}, [mode, ast]);
|
|
175
344
|
|
|
176
345
|
// Switch to Text: the seeding effect above serializes the AST once
|
|
177
346
|
// the editor's session is up.
|
|
@@ -183,13 +352,14 @@ export const ExpressionField = memo(
|
|
|
183
352
|
const enterVisualMode = useCallback(async () => {
|
|
184
353
|
if (parseTimer.current) clearTimeout(parseTimer.current);
|
|
185
354
|
if (mode === 'text' && text) {
|
|
186
|
-
const result = await dpqlRef.current?.parse?.(text);
|
|
355
|
+
const result = await dpqlRef.current?.parse?.(text, targetType);
|
|
356
|
+
readTypeCheck(result);
|
|
187
357
|
if (result?.success && result.expression) {
|
|
188
358
|
onChange(result.expression as IExpression);
|
|
189
359
|
}
|
|
190
360
|
}
|
|
191
361
|
setMode('visual');
|
|
192
|
-
}, [mode, text, onChange]);
|
|
362
|
+
}, [mode, text, onChange, targetType, readTypeCheck]);
|
|
193
363
|
|
|
194
364
|
useEffect(
|
|
195
365
|
() => () => {
|
|
@@ -198,8 +368,23 @@ export const ExpressionField = memo(
|
|
|
198
368
|
[]
|
|
199
369
|
);
|
|
200
370
|
|
|
371
|
+
/* What the Preview has to add: `none` until the server's rendering of the
|
|
372
|
+
text now showing exists, `repeats` when it reads exactly as that text does
|
|
373
|
+
(so it is not drawn — see below), `shown` otherwise. Exposed on the field,
|
|
374
|
+
because a Preview correctly left out leaves nothing else to observe. */
|
|
375
|
+
const previewState =
|
|
376
|
+
!preview || astText.trim() !== text.trim() ? 'none'
|
|
377
|
+
: dpqlDisplayedText(preview).trim() === dpqlDisplayedText(text).trim() ? 'repeats'
|
|
378
|
+
: 'shown';
|
|
379
|
+
|
|
201
380
|
return (
|
|
202
|
-
<ReqoreControlGroup
|
|
381
|
+
<ReqoreControlGroup
|
|
382
|
+
vertical
|
|
383
|
+
fluid
|
|
384
|
+
gapSize='small'
|
|
385
|
+
className='expression-field'
|
|
386
|
+
data-preview={mode === 'text' ? previewState : undefined}
|
|
387
|
+
>
|
|
203
388
|
<ReqoreControlGroup gapSize='small' fluid>
|
|
204
389
|
<ReqoreButton
|
|
205
390
|
icon='NodeTree'
|
|
@@ -232,27 +417,52 @@ export const ExpressionField = memo(
|
|
|
232
417
|
readOnly={readOnly}
|
|
233
418
|
height='48px'
|
|
234
419
|
/>
|
|
235
|
-
{/*
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
420
|
+
{/* What the server said about the result's type, when the field
|
|
421
|
+
declares one to check against.
|
|
422
|
+
|
|
423
|
+
Nothing is shown when the expression already fits, and nothing
|
|
424
|
+
is shown when the conversion is CERTAIN either — a number used
|
|
425
|
+
as text always works, and a warning about something that cannot
|
|
426
|
+
fail is what teaches people to ignore the ones that can. So this
|
|
427
|
+
appears for exactly two cases: the conversion is impossible
|
|
428
|
+
(danger), or it is attempted and may fail on the day (warning,
|
|
429
|
+
and the run checks the value itself). */}
|
|
430
|
+
{typeCheck && !typeCheck.compatible && (
|
|
431
|
+
<ReqoreMessage
|
|
432
|
+
intent={typeCheck.mayFail ? 'warning' : 'danger'}
|
|
433
|
+
size='small'
|
|
434
|
+
flat
|
|
435
|
+
opaque={false}
|
|
436
|
+
title={typeCheck.mayFail ? 'This may not fit' : 'This does not fit'}
|
|
437
|
+
>
|
|
438
|
+
{`The expression returns ${typeCheck.inferred}, and this field holds ${typeCheck.target}.`}
|
|
439
|
+
{typeCheck.mayFail ?
|
|
440
|
+
' That conversion is attempted rather than guaranteed, so the value itself is checked when it runs.'
|
|
441
|
+
: ''}
|
|
442
|
+
{typeCheck.fix ? (
|
|
443
|
+
<div style={{ marginTop: '6px' }}>
|
|
444
|
+
<DpqlRendering text={typeCheck.fix} data-testid='expression-type-fix' />
|
|
252
445
|
</div>
|
|
253
|
-
) :
|
|
254
|
-
|
|
255
|
-
|
|
446
|
+
) : null}
|
|
447
|
+
</ReqoreMessage>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{/* Shown only when it ADDS something.
|
|
451
|
+
|
|
452
|
+
It is a rendering of the AST, so for most text it repeats the
|
|
453
|
+
line directly above it — `1 + 2` previewing as `1 + 2` is a
|
|
454
|
+
second copy that flickers as you type. It earns the space when
|
|
455
|
+
the rendering DIFFERS: a normalised form, or wording the DPQL
|
|
456
|
+
does not have (`contains "es" (ignore case)`). Both are compared
|
|
457
|
+
as they are DRAWN — a reference is a chip in either, so the quotes
|
|
458
|
+
DPQL writes around one do not count — and ignoring surrounding
|
|
459
|
+
whitespace, which the renderer does not preserve.
|
|
460
|
+
|
|
461
|
+
An empty query has nothing to render either, so the box appears
|
|
462
|
+
once there is a result rather than holding a placeholder. */}
|
|
463
|
+
{previewState === 'shown' ? (
|
|
464
|
+
<ReqoreMessage intent='info' size='small' title='Preview' flat opaque={false}>
|
|
465
|
+
<DpqlRendering text={preview} data-testid='expression-preview' />
|
|
256
466
|
</ReqoreMessage>
|
|
257
467
|
) : null}
|
|
258
468
|
</>
|
|
@@ -268,6 +478,7 @@ export const ExpressionField = memo(
|
|
|
268
478
|
readOnly={readOnly}
|
|
269
479
|
localTemplates={localTemplates ?? { items: [] }}
|
|
270
480
|
serverHandled={serverHandled}
|
|
481
|
+
componentOverrides={componentOverrides}
|
|
271
482
|
extraActions={extraActions}
|
|
272
483
|
size={size}
|
|
273
484
|
reorder={reorder}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Copyright 2026 Qore Technologies, s.r.o.
|
|
2
|
+
/**
|
|
3
|
+
* Is this expression argument SUPPLIED, or is it still missing?
|
|
4
|
+
*
|
|
5
|
+
* DPQL has a null literal: `null` parses to `{exp: "value", args: [null]}` and
|
|
6
|
+
* round-trips, and an author writing it in the text editor means *"this is the
|
|
7
|
+
* value — nothing"*. It is the only way to assert that a call returned no
|
|
8
|
+
* value, which is how it was reported.
|
|
9
|
+
*
|
|
10
|
+
* The rule, stated once here because three different places had drawn it
|
|
11
|
+
* differently: **an argument that is explicitly null is supplied; one that is
|
|
12
|
+
* absent or `undefined` is still missing.**
|
|
13
|
+
*
|
|
14
|
+
* The builder used to ask `size(arg) === 0`, and lodash answers `0` for `null`
|
|
15
|
+
* as readily as for `{}`. So an explicit null was read as a missing argument
|
|
16
|
+
* and reset from the catalogue's declaration — which is how a saved draft ended
|
|
17
|
+
* up holding `{"type": "any", "required": true}`, an argument DEFINITION where
|
|
18
|
+
* its value should be, with the author's `null` gone and `required` (a schema
|
|
19
|
+
* key, never a value key) left behind to give it away.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True when `arg` carries no value yet — `undefined`, the `{}` the builder
|
|
24
|
+
* writes when a type change resets an argument, or any envelope with no
|
|
25
|
+
* `value` in it.
|
|
26
|
+
*
|
|
27
|
+
* `null` is NOT missing: it is the null literal.
|
|
28
|
+
*
|
|
29
|
+
* The rule used to be "an object with no keys at all", which let an envelope
|
|
30
|
+
* holding nothing but SCHEMA keys pass as a supplied value. That is how a saved
|
|
31
|
+
* draft kept `{type: 'any', required: true}` — an argument DEFINITION where the
|
|
32
|
+
* author's value belonged — and why re-opening the expression never repaired
|
|
33
|
+
* it: the builder looked at those two keys, concluded the argument was already
|
|
34
|
+
* supplied, and left the definition in place as data. `required` is a schema
|
|
35
|
+
* key; nothing ever reads it back off a stored value.
|
|
36
|
+
*
|
|
37
|
+
* Asking for the `value` instead makes the question the one that was always
|
|
38
|
+
* meant: has the author put something here?
|
|
39
|
+
*
|
|
40
|
+
* @param arg one entry of an expression's `args`
|
|
41
|
+
*/
|
|
42
|
+
export const isExpressionArgumentMissing = (arg: unknown): boolean => {
|
|
43
|
+
if (arg === null) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (arg === undefined) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof arg !== 'object') {
|
|
52
|
+
// a raw scalar argument — `0`, `false` and `''` are values too
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (arg as { value?: unknown }).value === undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The shape the builder stores an argument in: `{type, value}`.
|
|
61
|
+
*
|
|
62
|
+
* A parse result carries its arguments RAW — `args: [null]`, `args: [3]` —
|
|
63
|
+
* because that is what the grammar produced. The builder's editors read
|
|
64
|
+
* `arg.value`, so a raw argument has to be widened before it is stored, and
|
|
65
|
+
* widening `null` is the whole point: dropped instead, the author's null
|
|
66
|
+
* becomes an argument with no `value` key at all, which every later reader
|
|
67
|
+
* treats as an empty field.
|
|
68
|
+
*
|
|
69
|
+
* An argument that is already an envelope is returned untouched.
|
|
70
|
+
*
|
|
71
|
+
* @param arg one entry of an expression's `args`
|
|
72
|
+
* @param uiType the type the catalogue declares for that position
|
|
73
|
+
*/
|
|
74
|
+
export const asExpressionArgumentEnvelope = (arg: unknown, uiType?: string): unknown => {
|
|
75
|
+
if (arg !== null && (arg === undefined || typeof arg === 'object')) {
|
|
76
|
+
return arg;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { value: arg, ...(uiType ? { type: uiType } : {}) };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Does this value still look like an expression AST?
|
|
84
|
+
*
|
|
85
|
+
* `{exp, args}` is the shape the builder and `dpql/parse` both produce. The
|
|
86
|
+
* check is structural on purpose: the flag that marks an option as holding an
|
|
87
|
+
* expression describes its VALUE, so the value is what should decide whether
|
|
88
|
+
* the flag survives a change that said nothing about it.
|
|
89
|
+
*/
|
|
90
|
+
export const looksLikeExpressionAst = (value: unknown): boolean =>
|
|
91
|
+
!!value &&
|
|
92
|
+
typeof value === 'object' &&
|
|
93
|
+
!Array.isArray(value) &&
|
|
94
|
+
typeof (value as { exp?: unknown }).exp === 'string';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether an option should still be marked as holding an expression.
|
|
98
|
+
*
|
|
99
|
+
* The form engine used to delete the flag on ANY change that did not re-assert
|
|
100
|
+
* it, and most changes do not: an editor re-mounting, a revert, a validation
|
|
101
|
+
* pass. The value stayed the AST while the flag went, so the option degraded
|
|
102
|
+
* into a plain hash — which is how an assertion's Expected Value came back
|
|
103
|
+
* from a saved draft as a two-field object showing `exp` and `args`, and why
|
|
104
|
+
* `encodeTestValue` then stored it with `source: "literal"`.
|
|
105
|
+
*
|
|
106
|
+
* @param isFunction what the caller said: `true` sets the flag, `false` clears
|
|
107
|
+
* it deliberately (choosing a concrete type does this), `undefined` is no
|
|
108
|
+
* opinion at all
|
|
109
|
+
* @param value the option's new value
|
|
110
|
+
* @param wasExpression whether the option was marked before this change
|
|
111
|
+
*/
|
|
112
|
+
export const shouldMarkAsExpression = (
|
|
113
|
+
isFunction: boolean | undefined,
|
|
114
|
+
value: unknown,
|
|
115
|
+
wasExpression: boolean | undefined
|
|
116
|
+
): boolean => {
|
|
117
|
+
if (isFunction === true) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isFunction === false) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// No opinion: the flag belongs to the value, so it survives exactly as long
|
|
126
|
+
// as the value is still an expression.
|
|
127
|
+
return !!wasExpression && looksLikeExpressionAst(value);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Fill in an expression's missing arguments, leaving the supplied ones alone.
|
|
132
|
+
*
|
|
133
|
+
* Extracted from the builder so the decision it makes can be tested directly:
|
|
134
|
+
* a component test of the same behaviour passed just as happily with the defect
|
|
135
|
+
* in place, because the builder normalises on an interaction the harness never
|
|
136
|
+
* performed. A test that cannot fail is worse than none.
|
|
137
|
+
*
|
|
138
|
+
* @param expressions the served expression catalogue
|
|
139
|
+
* @param expression the chosen expression's `name`
|
|
140
|
+
* @param args the arguments so far — raw from a parse, or envelopes from the
|
|
141
|
+
* builder's own editors
|
|
142
|
+
*/
|
|
143
|
+
export const addMissingExpressionArgs = (
|
|
144
|
+
expressions: any[] | undefined,
|
|
145
|
+
expression: string,
|
|
146
|
+
args: any[] = []
|
|
147
|
+
): any[] => {
|
|
148
|
+
const newArgs = [...args];
|
|
149
|
+
const selected = expressions?.find((exp) => exp?.name === expression);
|
|
150
|
+
|
|
151
|
+
if (!selected) {
|
|
152
|
+
return newArgs;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (selected.varargs) {
|
|
156
|
+
if (selected.subtype === 2) {
|
|
157
|
+
newArgs.push({ is_expression: true, value: { args: [] } });
|
|
158
|
+
} else {
|
|
159
|
+
newArgs.push(
|
|
160
|
+
...Array.from({ length: (selected.min_args ?? 1) - 1 }, () => ({
|
|
161
|
+
type:
|
|
162
|
+
selected.args[0].ui_type === 'richtext' || selected.args[0].ui_type === 'number'
|
|
163
|
+
? selected.args[0].ui_type
|
|
164
|
+
: undefined,
|
|
165
|
+
}))
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return newArgs;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
selected.args.forEach((arg: any, index: number) => {
|
|
173
|
+
/* A RAW argument, as `dpql/parse` produces it — `args: [null]`, `args: [3]`
|
|
174
|
+
— is widened into the `{type, value}` envelope the editors read. Widening
|
|
175
|
+
the null is the point: dropped instead, the author's `null` became an
|
|
176
|
+
argument with no `value` key, and the branch below then filled the gap
|
|
177
|
+
from the catalogue's DECLARATION, which is how a saved draft came back
|
|
178
|
+
holding `{type: "any", required: true}` where its value belonged. */
|
|
179
|
+
newArgs[index] = asExpressionArgumentEnvelope(newArgs[index], arg.ui_type);
|
|
180
|
+
|
|
181
|
+
/* "Missing" already means "carries no value", so an argument waiting for
|
|
182
|
+
its catalogue default is simply a missing one — it needs no separate
|
|
183
|
+
test. */
|
|
184
|
+
if (!isExpressionArgumentMissing(newArgs[index])) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
newArgs[index] =
|
|
189
|
+
arg.default_value ?
|
|
190
|
+
{ value: arg.default_value, type: arg.ui_type }
|
|
191
|
+
: {
|
|
192
|
+
type:
|
|
193
|
+
arg.ui_type === 'richtext' || arg.ui_type === 'number' || arg.ui_type === 'bool' ?
|
|
194
|
+
arg.ui_type
|
|
195
|
+
: undefined,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return newArgs;
|
|
200
|
+
};
|