@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
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
IReqoreTextareaProps,
|
|
29
29
|
} from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
30
30
|
import { IQorusFormFieldSchemaBase, TQorusType } from '@qoretechnologies/ts-toolkit';
|
|
31
|
-
import { size } from 'lodash';
|
|
32
|
-
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
31
|
+
import { omit, size } from 'lodash';
|
|
32
|
+
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
33
33
|
import { useUpdateEffect } from 'react-use';
|
|
34
34
|
import {
|
|
35
35
|
filterTemplatesByType as templatesFilterFunc,
|
|
@@ -40,7 +40,14 @@ import {
|
|
|
40
40
|
describeTemplateReference,
|
|
41
41
|
isValueTemplate,
|
|
42
42
|
} from '../../../../helpers/templates';
|
|
43
|
+
import { classifyTypedText, mightBeDpqlExpression } from '../../../../helpers/dpqlDetection';
|
|
44
|
+
import {
|
|
45
|
+
renderTemplateItemDescriptions,
|
|
46
|
+
templateItemsToShow,
|
|
47
|
+
} from '../../../../helpers/templateItems';
|
|
48
|
+
import { useMarkdownRenderer } from '../../../Description/markdownRendererContext';
|
|
43
49
|
import { getTypeFromValue } from '../../../../helpers/validations';
|
|
50
|
+
import { useDpqlProbe } from '../../../dpqlEditor/useDpqlProbe';
|
|
44
51
|
import { useQorusTypes } from '../../../../hooks/useQorusTypes';
|
|
45
52
|
import { useWhyDidYouUpdate } from '../../../../hooks/useWhyDidYouUpdate';
|
|
46
53
|
import { ExpressionBuilder, IExpressionBuilderProps } from '../../expressions/builder';
|
|
@@ -57,11 +64,27 @@ import LongStringFormField from '../long-string/LongString';
|
|
|
57
64
|
import NumberFormField from '../number/Number';
|
|
58
65
|
import { ReadOnlyTemplateTag } from './ReadOnlyTemplateTag';
|
|
59
66
|
import { RichTextFormField } from '../rich-text/RichText';
|
|
67
|
+
import { richtextToString } from '../../../../helpers/common';
|
|
68
|
+
import { isSingleLineStringType } from '../../../../helpers/singleLineString';
|
|
69
|
+
import {
|
|
70
|
+
IRowMenuRegistration,
|
|
71
|
+
RowMenuContext,
|
|
72
|
+
useRowMenu,
|
|
73
|
+
useRowMenuPublisher,
|
|
74
|
+
} from '../../engine/rowMenuContext';
|
|
60
75
|
|
|
61
76
|
// Re-export template utilities for consumers
|
|
62
77
|
export { getTemplateKey, getTemplateValue, isValueTemplate };
|
|
63
78
|
export type { IQorusFormType as IQorusType };
|
|
64
79
|
|
|
80
|
+
/**
|
|
81
|
+
* How long the author must pause before typed text is sent to the DPQL
|
|
82
|
+
* probe. Long enough that a probe is not sent for every prefix of what is
|
|
83
|
+
* being typed, short enough that the offer appears while they are still
|
|
84
|
+
* looking at the field.
|
|
85
|
+
*/
|
|
86
|
+
const DPQL_DETECT_DEBOUNCE_MS = 400;
|
|
87
|
+
|
|
65
88
|
export const TemplatesListProps: IReqoreDropdownProps = {
|
|
66
89
|
useTargetWidth: true,
|
|
67
90
|
handler: 'focus',
|
|
@@ -160,6 +183,14 @@ export interface ITemplateFieldProps extends Partial<
|
|
|
160
183
|
* out of `rest` and never spread onto a leaf input.
|
|
161
184
|
*/
|
|
162
185
|
extraActions?: IExpressionBuilderProps['extraActions'];
|
|
186
|
+
/**
|
|
187
|
+
* SEAM (reqraft): the host's per-`ui_type` editors, forwarded to the field
|
|
188
|
+
* component and to the expression editor's operand rows. Declared here — not
|
|
189
|
+
* left to the index signature — for the same reason as `extraActions` above:
|
|
190
|
+
* so it is destructured out of `rest` and never spread onto a leaf input or a
|
|
191
|
+
* Reqore layout component, both of which pass unknown props to the DOM.
|
|
192
|
+
*/
|
|
193
|
+
componentOverrides?: Record<string, React.FC<any>>;
|
|
163
194
|
[key: string]: any;
|
|
164
195
|
default_value?: unknown;
|
|
165
196
|
}
|
|
@@ -297,10 +328,9 @@ export const CustomMenuItems = memo(
|
|
|
297
328
|
return (
|
|
298
329
|
<ReqoreMenuSection label='Set Custom Value' isCollapsed transparent icon='Text' {...rest}>
|
|
299
330
|
{items.map((menuItem, index) =>
|
|
300
|
-
'isDivider' in menuItem ?
|
|
331
|
+
'isDivider' in menuItem ?
|
|
301
332
|
<ReqoreMenuDivider key={index} {...menuItem} />
|
|
302
|
-
|
|
303
|
-
<ReqoreButton
|
|
333
|
+
: <ReqoreButton
|
|
304
334
|
{...(rest as any)}
|
|
305
335
|
{...menuItem}
|
|
306
336
|
key={index}
|
|
@@ -312,7 +342,6 @@ export const CustomMenuItems = memo(
|
|
|
312
342
|
});
|
|
313
343
|
}}
|
|
314
344
|
/>
|
|
315
|
-
)
|
|
316
345
|
)}
|
|
317
346
|
</ReqoreMenuSection>
|
|
318
347
|
);
|
|
@@ -331,6 +360,15 @@ export const TemplateDropdownSelector = memo(
|
|
|
331
360
|
size,
|
|
332
361
|
...rest
|
|
333
362
|
}: ITemplateDropdownSelectorProps) => {
|
|
363
|
+
/* The LAST hop before Reqore, which is the only place a drawn description
|
|
364
|
+
may be made: `filteredTemplates` upstream is a `JSON.stringify` memo key,
|
|
365
|
+
and an element in it throws on its own circular owner. */
|
|
366
|
+
const renderMarkdown = useMarkdownRenderer();
|
|
367
|
+
const shownItems = useMemo(
|
|
368
|
+
() => renderTemplateItemDescriptions(templateItemsToShow(items), renderMarkdown),
|
|
369
|
+
[items, renderMarkdown]
|
|
370
|
+
);
|
|
371
|
+
|
|
334
372
|
// One resolver for every surface that names a reference — the picker chip
|
|
335
373
|
// here, the read-only tag, and the compact row's expression summary. The
|
|
336
374
|
// row and this editor must agree: a reference that reads as its path when
|
|
@@ -364,7 +402,7 @@ export const TemplateDropdownSelector = memo(
|
|
|
364
402
|
minimal
|
|
365
403
|
compact
|
|
366
404
|
onItemSelect={onItemSelect}
|
|
367
|
-
items={
|
|
405
|
+
items={shownItems}
|
|
368
406
|
label={label}
|
|
369
407
|
leftIconProps={leftIconProps}
|
|
370
408
|
caretPosition='right'
|
|
@@ -372,7 +410,7 @@ export const TemplateDropdownSelector = memo(
|
|
|
372
410
|
size={size}
|
|
373
411
|
{...TemplatesListProps}
|
|
374
412
|
/>
|
|
375
|
-
{allowCustomValues || value ?
|
|
413
|
+
{allowCustomValues || value ?
|
|
376
414
|
<ReqoreButton
|
|
377
415
|
customTheme={TemplatesListProps.listCustomTheme}
|
|
378
416
|
fixed
|
|
@@ -384,15 +422,48 @@ export const TemplateDropdownSelector = memo(
|
|
|
384
422
|
size={size}
|
|
385
423
|
onClick={onRemoveClick}
|
|
386
424
|
/>
|
|
387
|
-
|
|
425
|
+
: null}
|
|
388
426
|
</ReqoreControlGroup>
|
|
389
427
|
</ReqoreControlGroup>
|
|
390
428
|
);
|
|
391
429
|
}
|
|
392
430
|
);
|
|
393
431
|
|
|
394
|
-
|
|
432
|
+
/**
|
|
433
|
+
* `ui_type`s whose OWN editor renders templates as chips inline.
|
|
434
|
+
*
|
|
435
|
+
* For these the template SELECTOR must not take over: it replaces a control the
|
|
436
|
+
* author can type in with one they can only pick from. A Qorus test assertion's
|
|
437
|
+
* `Value` is the case this was written for — the IDE's own `auto.tsx` already
|
|
438
|
+
* renders it as template rich text (type freely, references become chips), and
|
|
439
|
+
* the same field reached through this form engine offered a dropdown and no way
|
|
440
|
+
* to type at all.
|
|
441
|
+
*
|
|
442
|
+
* `richtext` is reqraft's own; a consumer adds its types through the
|
|
443
|
+
* `templateAwareUiTypes` prop, exactly as it adds `rendererOnlyUiTypes`.
|
|
444
|
+
*/
|
|
445
|
+
export const BuiltInTemplateAwareUiTypes = ['richtext'];
|
|
446
|
+
|
|
447
|
+
export const isTemplateAwareUiType = (uiType?: string, extra?: string[]): boolean =>
|
|
448
|
+
!!uiType && [...BuiltInTemplateAwareUiTypes, ...(extra ?? [])].includes(uiType);
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* One entry of the "set a value of this type" menu an untyped field offers.
|
|
452
|
+
*
|
|
453
|
+
* `onClick` is typed by what the caller actually invokes it with — the chosen
|
|
454
|
+
* value and a reset callback — rather than as `Function`, which accepts a class
|
|
455
|
+
* declaration as readily as a handler and gives no help at the call site.
|
|
456
|
+
*/
|
|
457
|
+
interface IUntypedFieldMenuItem {
|
|
458
|
+
label?: unknown;
|
|
459
|
+
description?: unknown;
|
|
460
|
+
isDivider?: boolean;
|
|
461
|
+
onClick?: (value: unknown, reset: () => void) => void;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const TemplateFieldImpl = memo(
|
|
395
465
|
({
|
|
466
|
+
rowMenu,
|
|
396
467
|
value,
|
|
397
468
|
name,
|
|
398
469
|
onChange,
|
|
@@ -403,6 +474,7 @@ export const TemplateField = memo(
|
|
|
403
474
|
allowFunctions,
|
|
404
475
|
allowTextExpressions,
|
|
405
476
|
extraActions,
|
|
477
|
+
componentOverrides,
|
|
406
478
|
allowCustomValues = true,
|
|
407
479
|
filterTemplatesByType = true,
|
|
408
480
|
filterTemplatesFunc,
|
|
@@ -418,7 +490,7 @@ export const TemplateField = memo(
|
|
|
418
490
|
reorder,
|
|
419
491
|
label,
|
|
420
492
|
...rest
|
|
421
|
-
}: ITemplateFieldProps) => {
|
|
493
|
+
}: ITemplateFieldProps & { rowMenu?: IRowMenuRegistration }) => {
|
|
422
494
|
const qorusTypes = useQorusTypes();
|
|
423
495
|
const functions = useExpressions({
|
|
424
496
|
allow: !!allowFunctions,
|
|
@@ -427,9 +499,22 @@ export const TemplateField = memo(
|
|
|
427
499
|
});
|
|
428
500
|
const type = rest.ui_type || rest.type || rest.defaultType;
|
|
429
501
|
|
|
502
|
+
/* The type an EXPRESSION must return, and is stored with, is a DATA type.
|
|
503
|
+
`type` above deliberately prefers `ui_type`, because that is what decides
|
|
504
|
+
which editor to render — but a ui_type says how a value is EDITED, not
|
|
505
|
+
what it is. The IDE's reference fields carry `ui_type: 'test-reference'`
|
|
506
|
+
(a rich-text editor that turns `$.` paths into chips), and feeding that
|
|
507
|
+
to the expression builder asked it for an expression RETURNING
|
|
508
|
+
`test-reference`: no expression returns one, so every choice was refused
|
|
509
|
+
with *"the expected return type is test-reference"*, and the ui_type was
|
|
510
|
+
then stored on the saved expression as its declared type.
|
|
511
|
+
|
|
512
|
+
The declared data type is the constraint; the ui_type is only a fallback
|
|
513
|
+
for a field that declares no type at all. */
|
|
514
|
+
const expressionDataType = rest.type || rest.defaultType || rest.ui_type;
|
|
515
|
+
|
|
430
516
|
const filteredTemplates = useMemo<IReqoreFormTemplates>(():
|
|
431
|
-
|
|
|
432
|
-
| undefined => {
|
|
517
|
+
IReqoreFormTemplates | undefined => {
|
|
433
518
|
if (!allowTemplates) {
|
|
434
519
|
return undefined;
|
|
435
520
|
}
|
|
@@ -444,7 +529,20 @@ export const TemplateField = memo(
|
|
|
444
529
|
result = filterTemplatesFunc(result);
|
|
445
530
|
}
|
|
446
531
|
|
|
447
|
-
|
|
532
|
+
/* A lone category is opened, once, HERE — where this control resolves the
|
|
533
|
+
templates every one of its pickers then reads: the "Select Template"
|
|
534
|
+
dropdown, the in-editor `$` list, the numeric field's focus dropdown and
|
|
535
|
+
the expression builder's argument picker. Applying it per picker meant
|
|
536
|
+
each new one had to remember, and the ones that forgot made the author
|
|
537
|
+
click through a header naming the only category on offer to reach the
|
|
538
|
+
only values on offer.
|
|
539
|
+
|
|
540
|
+
LAST, after both filters. `filterTemplatesFunc` is given the grouped
|
|
541
|
+
shape on purpose — the Qog expression builder filters top-level items by
|
|
542
|
+
`metadata.dataRole`, which is a property of the CATEGORY — so flattening
|
|
543
|
+
before it would hand it values and quietly change what it keeps. This
|
|
544
|
+
only changes what is finally shown. */
|
|
545
|
+
return { ...result, items: templateItemsToShow(result?.items) };
|
|
448
546
|
}, [
|
|
449
547
|
JSON.stringify(templates),
|
|
450
548
|
type,
|
|
@@ -469,24 +567,74 @@ export const TemplateField = memo(
|
|
|
469
567
|
// Only while the field is EMPTY. A field already holding a literal must
|
|
470
568
|
// open showing that literal — flipping to the template view would hide a
|
|
471
569
|
// value the author put there and make it look lost.
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
570
|
+
//
|
|
571
|
+
// A field that may hold a custom value opens there whether or not templates
|
|
572
|
+
// are on offer, and whether or not they have ARRIVED: template mode gives it
|
|
573
|
+
// a typable editor, which offers the list once there is one. Waiting for the
|
|
574
|
+
// list decided the landing from data that is async — on a cold load the
|
|
575
|
+
// type-filtered list did not exist at mount, so the field fell to the type
|
|
576
|
+
// picker and stayed there. A pick-only field (no custom values) is in
|
|
577
|
+
// template mode regardless, by the `!allowCustomValues` term.
|
|
475
578
|
const typeIsAnyLike = type === 'any' || type === 'auto';
|
|
476
|
-
|
|
579
|
+
/* An EMPTY field, for the purpose of offering the template selector.
|
|
580
|
+
|
|
581
|
+
`null` is deliberately not empty. It is the DPQL null literal — a value
|
|
582
|
+
an author writes to say "this is nothing", and the only way to assert
|
|
583
|
+
that a call returned no value. Counted as emptiness here, an argument
|
|
584
|
+
holding it was treated as a field the author had just cleared: with a
|
|
585
|
+
type of `any` and templates on offer, the selector-restore effect below
|
|
586
|
+
flipped the field into template mode with a null template value, and
|
|
587
|
+
that reported the field as cleared — so writing `null` in the Text tab
|
|
588
|
+
and switching to Visual silently erased it, leaving `{type: "any"}` in
|
|
589
|
+
the draft and the message "Value for argument 1 ("any") is invalid:
|
|
590
|
+
Value is empty".
|
|
591
|
+
|
|
592
|
+
`undefined` is the cleared state; `null` is a value. Same rule as
|
|
593
|
+
`expressions/argumentPresence`, which states it canonically. */
|
|
594
|
+
const isEmptyValue = value === undefined || value === '';
|
|
477
595
|
const hasTemplatesOnOffer = !!size(filteredTemplates?.items);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
596
|
+
/* The field's own editor already renders templates inline, so the selector
|
|
597
|
+
is not merely unnecessary here — it is a downgrade, swapping a typable
|
|
598
|
+
control for a pick-only one. Derived rather than folded into the state
|
|
599
|
+
below so every `setIsTemplate` path keeps working untouched; they simply
|
|
600
|
+
stop having anything to say for these fields. */
|
|
601
|
+
const editorHandlesTemplates =
|
|
602
|
+
!!allowTemplates && isTemplateAwareUiType(type as string, (rest as any).templateAwareUiTypes);
|
|
603
|
+
|
|
604
|
+
const hasOnlyAllowedValues = useMemo(
|
|
605
|
+
() => !!size(rest.allowed_values) && !rest.allowed_values_creatable,
|
|
606
|
+
[rest.allowed_values, rest.allowed_values_creatable]
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
/** Where this field lands when it is empty: an untyped one in template mode — see above. */
|
|
610
|
+
const emptyLandsOnTemplates =
|
|
611
|
+
typeIsAnyLike && (hasTemplatesOnOffer || (!!allowCustomValues && !hasOnlyAllowedValues));
|
|
612
|
+
const opensOnTemplates = isEmptyValue && emptyLandsOnTemplates;
|
|
613
|
+
|
|
614
|
+
const [isTemplateState, setIsTemplate] = useState<boolean>(
|
|
615
|
+
(isDefaultTemplate || isValueTemplate(value) || !allowCustomValues || opensOnTemplates) &&
|
|
483
616
|
allowTemplates
|
|
484
617
|
);
|
|
618
|
+
|
|
619
|
+
const isTemplate = editorHandlesTemplates ? false : isTemplateState;
|
|
485
620
|
const [internalIsFunction, setInternalIsFunction] = useState<boolean>(
|
|
486
621
|
!!isDefaultFunction && !!allowFunctions
|
|
487
622
|
);
|
|
488
623
|
const [templateValue, setTemplateValue] = useState<string | null>(value);
|
|
489
624
|
|
|
625
|
+
// Typed text that turned out to be an expression the author may accept.
|
|
626
|
+
const [dpqlOffer, setDpqlOffer] = useState<{ text: string; expression: any } | null>(null);
|
|
627
|
+
// The literal this field held before typed text switched it into
|
|
628
|
+
// expression mode — kept so the switch is undoable, and so the editor
|
|
629
|
+
// opens on the Text view the author was already writing in.
|
|
630
|
+
const [expressionFromText, setExpressionFromText] = useState<string | null>(null);
|
|
631
|
+
// Texts the author has said no to. Without this, dismissing an offer (or
|
|
632
|
+
// undoing a switch) would re-detect the same text on the next render and
|
|
633
|
+
// ask again forever.
|
|
634
|
+
const declinedTexts = useRef<Set<string>>(new Set());
|
|
635
|
+
const detectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
636
|
+
const probeDpql = useDpqlProbe();
|
|
637
|
+
|
|
490
638
|
const effectiveIsFunction = isFunction || internalIsFunction;
|
|
491
639
|
|
|
492
640
|
useWhyDidYouUpdate(`Template field ${name}`, {
|
|
@@ -502,6 +650,63 @@ export const TemplateField = memo(
|
|
|
502
650
|
}
|
|
503
651
|
}, [JSON.stringify(value)]);
|
|
504
652
|
|
|
653
|
+
/* Clearing an untyped field returns it to the template selector, which is
|
|
654
|
+
where mounting it empty already lands.
|
|
655
|
+
|
|
656
|
+
The rule above that opens an `any`-like field on the template selector is
|
|
657
|
+
a `useState` INITIALISER, so it only ever ran on mount. Clearing a value
|
|
658
|
+
in place does not remount anything, so the field fell through to the TYPE
|
|
659
|
+
picker and demanded `string`/`int`/`hash` before it would let the author
|
|
660
|
+
name a value they had already captured -- the very question that rule
|
|
661
|
+
exists to stop asking. Reloading the page fixed it, which is the tell:
|
|
662
|
+
same field, same empty value, different landing, because only one of the
|
|
663
|
+
two paths ran the rule.
|
|
664
|
+
|
|
665
|
+
Watched as a TRANSITION from a value to no value, not as "the value is
|
|
666
|
+
empty": an author who picks `Set Custom Value` on an empty field is
|
|
667
|
+
asking for the type picker, and re-asserting the template view on every
|
|
668
|
+
render while the field sat empty would take it away again immediately. */
|
|
669
|
+
const hadValue = useRef(!isEmptyValue);
|
|
670
|
+
/* A clear is REMEMBERED rather than acted on in the same render.
|
|
671
|
+
|
|
672
|
+
While the field holds a value its resolved type is that VALUE's type — a
|
|
673
|
+
reference reads as `test-reference`, not `auto` — and clearing reverts it
|
|
674
|
+
to the schema's `auto` one render LATER than the value empties. Acting
|
|
675
|
+
only on the transition therefore ran while `typeIsAnyLike` was still
|
|
676
|
+
false and did nothing, and by the time the type settled the transition
|
|
677
|
+
had passed. Measured in the live IDE at the moment of the clear:
|
|
678
|
+
`{ wasCleared: true, typeIsAnyLike: false, type: "test-reference",
|
|
679
|
+
hasTemplatesOnOffer: true }`, then `typeIsAnyLike: true` on the very next
|
|
680
|
+
render with `wasCleared` already false. */
|
|
681
|
+
const restoreSelectorWhenSettled = useRef(false);
|
|
682
|
+
/** Set by the template control's own `×`, which is an author saying "not a
|
|
683
|
+
* template" — that clear must not be answered with the selector again. */
|
|
684
|
+
const suppressSelectorRestore = useRef(false);
|
|
685
|
+
|
|
686
|
+
useEffect(() => {
|
|
687
|
+
if (hadValue.current && isEmptyValue) {
|
|
688
|
+
restoreSelectorWhenSettled.current = !suppressSelectorRestore.current;
|
|
689
|
+
suppressSelectorRestore.current = false;
|
|
690
|
+
}
|
|
691
|
+
if (!isEmptyValue) {
|
|
692
|
+
// A value arrived: whatever the author did, they are not sitting on an
|
|
693
|
+
// empty field waiting to be offered the list.
|
|
694
|
+
restoreSelectorWhenSettled.current = false;
|
|
695
|
+
}
|
|
696
|
+
hadValue.current = !isEmptyValue;
|
|
697
|
+
|
|
698
|
+
if (
|
|
699
|
+
restoreSelectorWhenSettled.current &&
|
|
700
|
+
opensOnTemplates &&
|
|
701
|
+
allowTemplates &&
|
|
702
|
+
!effectiveIsFunction
|
|
703
|
+
) {
|
|
704
|
+
restoreSelectorWhenSettled.current = false;
|
|
705
|
+
setTemplateValue(null);
|
|
706
|
+
setIsTemplate(true);
|
|
707
|
+
}
|
|
708
|
+
}, [isEmptyValue, opensOnTemplates, allowTemplates, effectiveIsFunction]);
|
|
709
|
+
|
|
505
710
|
useEffect(() => {
|
|
506
711
|
if (allowCustomValues && isTemplate && value && !isValueTemplate(value)) {
|
|
507
712
|
setIsTemplate(false);
|
|
@@ -522,6 +727,17 @@ export const TemplateField = memo(
|
|
|
522
727
|
return;
|
|
523
728
|
}
|
|
524
729
|
|
|
730
|
+
// A template reference with an OPERATOR on it (`$local:count + 1`) is
|
|
731
|
+
// an expression, not a template. The loose check above passes it —
|
|
732
|
+
// it starts with `$` and has a colon — so without this every such
|
|
733
|
+
// string was swallowed into template mode, where the arithmetic is
|
|
734
|
+
// dead text and no affordance can reach it. A token standing alone
|
|
735
|
+
// (`$local:count`, `$data:{1.field}`) has no operator and still
|
|
736
|
+
// flips, which is the case template mode is for.
|
|
737
|
+
if (!isCompleteTemplateToken(value) && mightBeDpqlExpression(value)) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
525
741
|
setIsTemplate(true);
|
|
526
742
|
setTemplateValue(value);
|
|
527
743
|
}
|
|
@@ -534,11 +750,6 @@ export const TemplateField = memo(
|
|
|
534
750
|
}
|
|
535
751
|
}, [JSON.stringify(templateValue)]);
|
|
536
752
|
|
|
537
|
-
const hasOnlyAllowedValues = useMemo(
|
|
538
|
-
() => !!size(rest.allowed_values) && !rest.allowed_values_creatable,
|
|
539
|
-
[rest.allowed_values, rest.allowed_values_creatable]
|
|
540
|
-
);
|
|
541
|
-
|
|
542
753
|
const showTemplateToggle = allowCustomValues && allowTemplates && !rest.arg_schema;
|
|
543
754
|
|
|
544
755
|
// Only a BRACED context ref (`$data:{…}` — machine-written, nobody types
|
|
@@ -550,8 +761,23 @@ export const TemplateField = memo(
|
|
|
550
761
|
// the rich-text string mode.
|
|
551
762
|
const templateValueIsBracedToken = isBracedTemplateToken(templateValue);
|
|
552
763
|
|
|
764
|
+
/* An UNTYPED field can be typed into as well as picked from.
|
|
765
|
+
|
|
766
|
+
Template mode has a typable editor whenever the field's type can hold
|
|
767
|
+
arbitrary text, and `any`/`auto` can — it is the type that has not been
|
|
768
|
+
narrowed yet, not a type that excludes text. Restricting this to
|
|
769
|
+
`string` meant an empty untyped field opened on template mode with no
|
|
770
|
+
editor to offer, so it fell through to the pick-only "Select Template"
|
|
771
|
+
dropdown: a control that can only choose from a list, on exactly the
|
|
772
|
+
fields where an author most often needs to write something the list
|
|
773
|
+
cannot hold — a deeper walk, a literal, or an expression.
|
|
774
|
+
|
|
775
|
+
Reported against a Qorus assertion's Expected Value, and it was never
|
|
776
|
+
specific to that field: every empty untyped field with templates on
|
|
777
|
+
offer got the same downgrade. The editor still offers the same templates
|
|
778
|
+
on focus, so nothing is lost by being able to type as well. */
|
|
553
779
|
const templateSupportsCustomValues =
|
|
554
|
-
allowCustomValues && type === 'string' && !hasOnlyAllowedValues;
|
|
780
|
+
allowCustomValues && (type === 'string' || typeIsAnyLike) && !hasOnlyAllowedValues;
|
|
555
781
|
const showTemplatesDropdown =
|
|
556
782
|
allowTemplates && (!allowCustomValues || (isTemplate && !templateSupportsCustomValues));
|
|
557
783
|
const hasOnlyExpressions = !allowCustomValues && !allowTemplates && allowFunctions;
|
|
@@ -572,18 +798,28 @@ export const TemplateField = memo(
|
|
|
572
798
|
const componentTypeProp = componentFromType ? { type } : {};
|
|
573
799
|
const fieldAriaLabel = rest['aria-label'] ?? label ?? rest.display_name ?? name;
|
|
574
800
|
|
|
575
|
-
|
|
576
801
|
const handleTemplateFieldChange = useCallback(
|
|
577
802
|
(_name: string, val: string) => {
|
|
578
803
|
if (!val) {
|
|
579
|
-
|
|
804
|
+
/* Emptying the text leaves the field where an empty field lands. For
|
|
805
|
+
an untyped field that is THIS editor: switching to custom mode here
|
|
806
|
+
mounted a different editor in its place, so deleting the last
|
|
807
|
+
character lost the cursor. */
|
|
808
|
+
if (!(emptyLandsOnTemplates && allowTemplates)) {
|
|
809
|
+
setIsTemplate(false);
|
|
810
|
+
}
|
|
580
811
|
setTemplateValue(null);
|
|
581
812
|
onChange(name, undefined);
|
|
582
813
|
} else {
|
|
583
814
|
setTemplateValue(val);
|
|
584
815
|
}
|
|
585
816
|
},
|
|
586
|
-
[name, onChange]
|
|
817
|
+
[name, onChange, emptyLandsOnTemplates, allowTemplates]
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const handleTemplateTextChange = useCallback(
|
|
821
|
+
(val: unknown) => handleTemplateFieldChange(name, typeof val === 'string' ? val : ''),
|
|
822
|
+
[handleTemplateFieldChange, name]
|
|
587
823
|
);
|
|
588
824
|
|
|
589
825
|
const handleSelectTemplateFromList = useCallback(
|
|
@@ -591,24 +827,38 @@ export const TemplateField = memo(
|
|
|
591
827
|
(item) => {
|
|
592
828
|
// If the template type is richtext, we need to wrap the template value in the richtext template format
|
|
593
829
|
const value =
|
|
594
|
-
item.badge === 'richtext'
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
830
|
+
item.badge === 'richtext' ?
|
|
831
|
+
([
|
|
832
|
+
{
|
|
833
|
+
type: 'paragraph',
|
|
834
|
+
/* The empty text nodes on either side are load-bearing. A
|
|
835
|
+
chip is an inline VOID: it holds no text of its own, so the
|
|
836
|
+
only places a cursor can go are the text nodes AROUND it.
|
|
837
|
+
Slate treats that as an invariant and repairs it during
|
|
838
|
+
normalisation, which runs on edits but NOT on a document
|
|
839
|
+
handed in whole as a controlled value — which is what this
|
|
840
|
+
is. Without them the picked template renders as a chip that
|
|
841
|
+
cannot be typed after, so it can never be extended by hand. */
|
|
842
|
+
children: [
|
|
843
|
+
{ text: '' },
|
|
844
|
+
{
|
|
845
|
+
children: [{ text: '' }],
|
|
846
|
+
label: item.label,
|
|
847
|
+
type: 'tag',
|
|
848
|
+
value: item.value,
|
|
849
|
+
metadata: item.metadata,
|
|
850
|
+
},
|
|
851
|
+
{ text: '' },
|
|
852
|
+
],
|
|
853
|
+
},
|
|
854
|
+
] as IReqoreRichTextEditorProps['value'])
|
|
855
|
+
: item.value;
|
|
856
|
+
|
|
857
|
+
onChange(
|
|
858
|
+
name,
|
|
859
|
+
value,
|
|
860
|
+
hasOnlyAllowedValues ? (type as TQorusType) : (item.badge as TQorusType)
|
|
861
|
+
);
|
|
612
862
|
},
|
|
613
863
|
[name, onChange]
|
|
614
864
|
);
|
|
@@ -618,7 +868,15 @@ export const TemplateField = memo(
|
|
|
618
868
|
// IDE-only, so the menu item is dropped (`allowSaving` is inert).
|
|
619
869
|
|
|
620
870
|
const handleRemoveTemplateClick = useCallback(() => {
|
|
871
|
+
/* This is the `×` ON the template control, and it means "I do not want a
|
|
872
|
+
template here" — on a field whose menu offers no `Set Custom Value` it
|
|
873
|
+
is the ONLY way to reach a literal. So it still drops to the custom
|
|
874
|
+
value editor, and it tells the empty-field rule below to keep its hands
|
|
875
|
+
off this particular clear: that rule watches the value going away, and
|
|
876
|
+
this handler clears the value too, so without the flag it would send
|
|
877
|
+
the author straight back to the selector they just dismissed. */
|
|
621
878
|
if (allowCustomValues) {
|
|
879
|
+
suppressSelectorRestore.current = true;
|
|
622
880
|
setIsTemplate(false);
|
|
623
881
|
}
|
|
624
882
|
|
|
@@ -668,12 +926,245 @@ export const TemplateField = memo(
|
|
|
668
926
|
(expressionValue: IExpression | undefined, remove: boolean) => {
|
|
669
927
|
if (remove) {
|
|
670
928
|
setInternalIsFunction(false);
|
|
929
|
+
// The expression is gone, so there is no longer a switch to undo.
|
|
930
|
+
setExpressionFromText(null);
|
|
671
931
|
}
|
|
672
|
-
onChange(name, expressionValue?.value,
|
|
932
|
+
onChange(name, expressionValue?.value, expressionDataType as TQorusType, !remove);
|
|
933
|
+
},
|
|
934
|
+
[name, onChange, expressionDataType, value]
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
// ─── Text typed into a plain field that is really a DPQL expression ───
|
|
938
|
+
//
|
|
939
|
+
// A field that accepts expressions still gets DPQL TYPED into it, in the
|
|
940
|
+
// ordinary editor, by an author who never opened the expression view.
|
|
941
|
+
// What happens then depends on the field's own type, and the asymmetry is
|
|
942
|
+
// deliberate: text that could stand as a literal here is only ever
|
|
943
|
+
// OFFERED, because silently rewriting the string `a + b` into a
|
|
944
|
+
// concatenation would destroy a value the author meant. Text that could
|
|
945
|
+
// NOT be a literal here was already an error the moment it was typed, so
|
|
946
|
+
// switching costs nothing and explains the error.
|
|
947
|
+
//
|
|
948
|
+
// The server decides whether the text is an expression at all — see
|
|
949
|
+
// `helpers/dpqlDetection` for why a successful parse alone means nothing.
|
|
950
|
+
const validationField = useMemo(
|
|
951
|
+
() => ({
|
|
952
|
+
validation_regex: rest.validation_regex,
|
|
953
|
+
has_to_be_valid_identifier: rest.has_to_be_valid_identifier,
|
|
954
|
+
has_to_have_value: rest.has_to_have_value,
|
|
955
|
+
rules: rest.rules,
|
|
956
|
+
arg_schema: rest.arg_schema,
|
|
957
|
+
}),
|
|
958
|
+
[
|
|
959
|
+
rest.validation_regex,
|
|
960
|
+
rest.has_to_be_valid_identifier,
|
|
961
|
+
rest.has_to_have_value,
|
|
962
|
+
JSON.stringify(rest.rules),
|
|
963
|
+
JSON.stringify(rest.arg_schema),
|
|
964
|
+
]
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
const enterExpressionFromText = useCallback(
|
|
968
|
+
(text: string, expression: any) => {
|
|
969
|
+
setDpqlOffer(null);
|
|
970
|
+
setExpressionFromText(text);
|
|
971
|
+
setInternalIsFunction(true);
|
|
972
|
+
setIsTemplate(false);
|
|
973
|
+
setTemplateValue(null);
|
|
974
|
+
// `dpql/parse` answers with the field-ready envelope
|
|
975
|
+
// (`{is_expression, value}`); this field stores the inner AST and
|
|
976
|
+
// signals the flag through `onChange`'s fourth argument, exactly as
|
|
977
|
+
// `handleExpressionChange` does.
|
|
978
|
+
onChange?.(name, expression?.value ?? expression, expressionDataType as TQorusType, true);
|
|
673
979
|
},
|
|
674
|
-
[name, onChange,
|
|
980
|
+
[name, onChange, expressionDataType]
|
|
675
981
|
);
|
|
676
982
|
|
|
983
|
+
const undoExpressionFromText = useCallback(() => {
|
|
984
|
+
if (expressionFromText === null) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
declinedTexts.current.add(expressionFromText);
|
|
989
|
+
setInternalIsFunction(false);
|
|
990
|
+
setExpressionFromText(null);
|
|
991
|
+
onChange?.(name, expressionFromText, type as TQorusType, false);
|
|
992
|
+
}, [expressionFromText, name, onChange, type]);
|
|
993
|
+
|
|
994
|
+
const declineDpqlOffer = useCallback(() => {
|
|
995
|
+
if (dpqlOffer) {
|
|
996
|
+
declinedTexts.current.add(dpqlOffer.text);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
setDpqlOffer(null);
|
|
1000
|
+
}, [dpqlOffer]);
|
|
1001
|
+
|
|
1002
|
+
const acceptDpqlOffer = useCallback(() => {
|
|
1003
|
+
if (dpqlOffer) {
|
|
1004
|
+
enterExpressionFromText(dpqlOffer.text, dpqlOffer.expression);
|
|
1005
|
+
}
|
|
1006
|
+
}, [dpqlOffer, enterExpressionFromText]);
|
|
1007
|
+
|
|
1008
|
+
// Held in a ref so the debounce below does not depend on it. `onChange`
|
|
1009
|
+
// comes from the host form and can be a fresh function on any render;
|
|
1010
|
+
// with the callback in the effect's deps, a re-render caused by a
|
|
1011
|
+
// DIFFERENT field would restart this field's timer, and a form the
|
|
1012
|
+
// author is typing in re-renders constantly.
|
|
1013
|
+
const enterExpressionRef = useRef(enterExpressionFromText);
|
|
1014
|
+
useEffect(() => {
|
|
1015
|
+
enterExpressionRef.current = enterExpressionFromText;
|
|
1016
|
+
}, [enterExpressionFromText]);
|
|
1017
|
+
|
|
1018
|
+
// Detection only runs on a field that could hold an expression and is
|
|
1019
|
+
// currently showing a plain editor — never on a read-only field, a
|
|
1020
|
+
// fixed-value list, or one already in template or expression mode.
|
|
1021
|
+
const canDetectDpql =
|
|
1022
|
+
!!allowFunctions &&
|
|
1023
|
+
!!allowTextExpressions &&
|
|
1024
|
+
!effectiveIsFunction &&
|
|
1025
|
+
!isTemplate &&
|
|
1026
|
+
!hasOnlyAllowedValues &&
|
|
1027
|
+
!rest.readonly &&
|
|
1028
|
+
!rest.readOnly &&
|
|
1029
|
+
!rest.disabled;
|
|
1030
|
+
|
|
1031
|
+
useEffect(() => {
|
|
1032
|
+
if (detectTimer.current) {
|
|
1033
|
+
clearTimeout(detectTimer.current);
|
|
1034
|
+
detectTimer.current = null;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/* A richtext field is still text. It hands its value back as a Slate
|
|
1038
|
+
DOCUMENT rather than a flattened string — deliberately, so a chosen
|
|
1039
|
+
reference cannot fuse with text typed beside it — and reading only
|
|
1040
|
+
`typeof value === 'string'` here silently switched detection off for
|
|
1041
|
+
every one of them the moment the field changed shape. A test
|
|
1042
|
+
assertion's Value is exactly that field, and `1 + 2` typed into it
|
|
1043
|
+
stopped being offered as an expression.
|
|
1044
|
+
|
|
1045
|
+
`richtextToString` flattens a tag to its raw value, so a document
|
|
1046
|
+
holding only a chosen reference reads as `$.result` — no operator, so
|
|
1047
|
+
`mightBeDpqlExpression` rejects it below and the server is never
|
|
1048
|
+
asked. */
|
|
1049
|
+
const text =
|
|
1050
|
+
typeof value === 'string' ? value
|
|
1051
|
+
: Array.isArray(value) ? richtextToString(value as never)
|
|
1052
|
+
: undefined;
|
|
1053
|
+
|
|
1054
|
+
if (
|
|
1055
|
+
!canDetectDpql ||
|
|
1056
|
+
!text ||
|
|
1057
|
+
declinedTexts.current.has(text) ||
|
|
1058
|
+
!mightBeDpqlExpression(text)
|
|
1059
|
+
) {
|
|
1060
|
+
setDpqlOffer(null);
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
let cancelled = false;
|
|
1065
|
+
|
|
1066
|
+
detectTimer.current = setTimeout(() => {
|
|
1067
|
+
detectTimer.current = null;
|
|
1068
|
+
|
|
1069
|
+
void probeDpql(text)
|
|
1070
|
+
.then((parsed) => {
|
|
1071
|
+
if (cancelled) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const outcome = classifyTypedText({ text, type, field: validationField, parsed });
|
|
1076
|
+
|
|
1077
|
+
if (outcome === 'switch') {
|
|
1078
|
+
enterExpressionRef.current(text, parsed.expression);
|
|
1079
|
+
} else if (outcome === 'offer') {
|
|
1080
|
+
setDpqlOffer({ text, expression: parsed.expression });
|
|
1081
|
+
} else {
|
|
1082
|
+
setDpqlOffer(null);
|
|
1083
|
+
}
|
|
1084
|
+
})
|
|
1085
|
+
.catch(() => {
|
|
1086
|
+
// Detection is an offer of help, never a reason for a field to
|
|
1087
|
+
// break: an unreachable instance or a classifier that threw leaves
|
|
1088
|
+
// the author's text exactly where they put it.
|
|
1089
|
+
if (!cancelled) {
|
|
1090
|
+
setDpqlOffer(null);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}, DPQL_DETECT_DEBOUNCE_MS);
|
|
1094
|
+
|
|
1095
|
+
return () => {
|
|
1096
|
+
cancelled = true;
|
|
1097
|
+
|
|
1098
|
+
if (detectTimer.current) {
|
|
1099
|
+
clearTimeout(detectTimer.current);
|
|
1100
|
+
detectTimer.current = null;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
}, [value, canDetectDpql, type, validationField, probeDpql]);
|
|
1104
|
+
|
|
1105
|
+
const canOfferExpression =
|
|
1106
|
+
allowFunctions && !hasOnlyAllowedValues && !rest.readonly && !internalIsFunction;
|
|
1107
|
+
const canOfferTemplate = showTemplateToggle && !isTemplate;
|
|
1108
|
+
|
|
1109
|
+
const publishedItems = useMemo(
|
|
1110
|
+
() => [
|
|
1111
|
+
...(canOfferExpression && !functions.loading
|
|
1112
|
+
? [
|
|
1113
|
+
{
|
|
1114
|
+
label: 'Use Expression',
|
|
1115
|
+
icon: 'Functions' as const,
|
|
1116
|
+
tooltip: 'Run a function on this value',
|
|
1117
|
+
onClick: handleSelectFunctionChange,
|
|
1118
|
+
},
|
|
1119
|
+
]
|
|
1120
|
+
: []),
|
|
1121
|
+
...(canOfferTemplate
|
|
1122
|
+
? [
|
|
1123
|
+
{
|
|
1124
|
+
label: 'Use Template',
|
|
1125
|
+
icon: 'MoneyDollarCircleLine' as const,
|
|
1126
|
+
tooltip: 'Use a template',
|
|
1127
|
+
onClick: handleTemplateToggleClick,
|
|
1128
|
+
},
|
|
1129
|
+
]
|
|
1130
|
+
: []),
|
|
1131
|
+
/* The "set a value of this type" choices an untyped field offers.
|
|
1132
|
+
Dividers are dropped: they grouped items in a menu this field drew
|
|
1133
|
+
itself, and in the row's shared menu they would divide other
|
|
1134
|
+
people's. */
|
|
1135
|
+
...((menuItems ?? []) as IUntypedFieldMenuItem[])
|
|
1136
|
+
.filter((item) => !('isDivider' in item))
|
|
1137
|
+
.map((item) => ({
|
|
1138
|
+
label: item.label as string,
|
|
1139
|
+
description: item.description as string | undefined,
|
|
1140
|
+
onClick: () =>
|
|
1141
|
+
item.onClick?.(undefined, () => {
|
|
1142
|
+
setIsTemplate(false);
|
|
1143
|
+
setTemplateValue(null);
|
|
1144
|
+
}),
|
|
1145
|
+
})),
|
|
1146
|
+
],
|
|
1147
|
+
[
|
|
1148
|
+
canOfferExpression,
|
|
1149
|
+
functions.loading,
|
|
1150
|
+
canOfferTemplate,
|
|
1151
|
+
menuItems,
|
|
1152
|
+
handleSelectFunctionChange,
|
|
1153
|
+
handleTemplateToggleClick,
|
|
1154
|
+
]
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
/* Keyed on WHICH items are offered, never on the items: they carry
|
|
1158
|
+
handlers, so they are a new array every render and a row comparing them
|
|
1159
|
+
by value would loop. */
|
|
1160
|
+
const publishedKey = [
|
|
1161
|
+
canOfferExpression && !functions.loading ? 'expression' : '',
|
|
1162
|
+
canOfferTemplate ? 'template' : '',
|
|
1163
|
+
`custom:${((menuItems ?? []) as { label?: unknown }[]).map((item) => String(item.label ?? '')).join('|')}`,
|
|
1164
|
+
].join(',');
|
|
1165
|
+
|
|
1166
|
+
useRowMenuPublisher(rowMenu, publishedKey, publishedItems as never);
|
|
1167
|
+
|
|
677
1168
|
const renderControls = useCallback(() => {
|
|
678
1169
|
const showFunctionsDropdown =
|
|
679
1170
|
allowFunctions && !hasOnlyAllowedValues && !rest.readonly && !internalIsFunction;
|
|
@@ -681,33 +1172,44 @@ export const TemplateField = memo(
|
|
|
681
1172
|
// The "Set value" label promises a way to set one — reorder rows alone don't.
|
|
682
1173
|
const hasValueRows = showFunctionsDropdown || showTemplatesButton || size(menuItems) > 0;
|
|
683
1174
|
|
|
1175
|
+
/* ONE menu per control. Where this field sits in a form ROW, the row
|
|
1176
|
+
already renders a ⋮ of its own, and drawing a second one beside it
|
|
1177
|
+
put two menus of slightly different widths on the same control —
|
|
1178
|
+
with the type choices an `any` field offers hidden in the narrower
|
|
1179
|
+
one. The items are published to the row instead (see
|
|
1180
|
+
`rowMenuContext`); outside a row there is nothing to publish into,
|
|
1181
|
+
so the field goes on drawing its own. */
|
|
1182
|
+
if (rowMenu) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
684
1186
|
if (hasOnlyExpressions) {
|
|
685
|
-
return
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
<ReqoreButton
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
)
|
|
1187
|
+
return (
|
|
1188
|
+
showFunctionsDropdown ?
|
|
1189
|
+
functions.loading ?
|
|
1190
|
+
<ReqoreSkeleton size={rest.size} />
|
|
1191
|
+
: <ReqoreButton
|
|
1192
|
+
compact
|
|
1193
|
+
minimal
|
|
1194
|
+
label='Create New Expression'
|
|
1195
|
+
className='function-selector'
|
|
1196
|
+
icon='Functions'
|
|
1197
|
+
tooltip='This field only accepts expressions'
|
|
1198
|
+
onClick={() => {
|
|
1199
|
+
setIsTemplate(false);
|
|
1200
|
+
setTemplateValue(null);
|
|
1201
|
+
onChange?.(
|
|
1202
|
+
name,
|
|
1203
|
+
{
|
|
1204
|
+
args: [],
|
|
1205
|
+
},
|
|
1206
|
+
undefined,
|
|
1207
|
+
true
|
|
1208
|
+
);
|
|
1209
|
+
}}
|
|
1210
|
+
/>
|
|
1211
|
+
: null
|
|
1212
|
+
);
|
|
711
1213
|
}
|
|
712
1214
|
|
|
713
1215
|
if (hasValueRows || size(menuActions?.items) > 0) {
|
|
@@ -728,10 +1230,18 @@ export const TemplateField = memo(
|
|
|
728
1230
|
transparent: true,
|
|
729
1231
|
size: rest.size,
|
|
730
1232
|
fixed: true,
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1233
|
+
/* Centre the trailing menu in its flex line so it lines up
|
|
1234
|
+
with sibling action buttons (reqore alignSelf; replaces a
|
|
1235
|
+
reqraft `align-self !important` override of this button).
|
|
1236
|
+
|
|
1237
|
+
Centring is only right beside a ONE-LINE editor. The
|
|
1238
|
+
expression shell is a toolbar with an editor, a type message
|
|
1239
|
+
and a preview stacked under it, so centring put this menu
|
|
1240
|
+
somewhere down the side of that block — level with nothing,
|
|
1241
|
+
over the editor, and a long way from the Undo it belongs
|
|
1242
|
+
beside. There it goes to the top, which is where the shell's
|
|
1243
|
+
own toolbar is. */
|
|
1244
|
+
alignSelf: effectiveIsFunction ? 'flex-start' : 'center',
|
|
735
1245
|
label: hasInputAffordance || !hasValueRows ? undefined : 'Set value',
|
|
736
1246
|
style:
|
|
737
1247
|
hasInputAffordance ?
|
|
@@ -757,11 +1267,10 @@ export const TemplateField = memo(
|
|
|
757
1267
|
handler='click'
|
|
758
1268
|
content={
|
|
759
1269
|
<ReqoreMenu size={rest.size} maxHeight='400px' style={{ overflow: 'auto' }}>
|
|
760
|
-
{showFunctionsDropdown ?
|
|
761
|
-
functions.loading ?
|
|
1270
|
+
{showFunctionsDropdown ?
|
|
1271
|
+
functions.loading ?
|
|
762
1272
|
<ReqoreSkeleton size={rest.size} />
|
|
763
|
-
|
|
764
|
-
<ReqoreButton
|
|
1273
|
+
: <ReqoreButton
|
|
765
1274
|
compact
|
|
766
1275
|
transparent
|
|
767
1276
|
label='Use Expression'
|
|
@@ -770,10 +1279,10 @@ export const TemplateField = memo(
|
|
|
770
1279
|
tooltip='Run a function on this value'
|
|
771
1280
|
onClick={handleSelectFunctionChange}
|
|
772
1281
|
/>
|
|
773
|
-
)
|
|
774
|
-
) : null}
|
|
775
1282
|
|
|
776
|
-
|
|
1283
|
+
: null}
|
|
1284
|
+
|
|
1285
|
+
{showTemplatesButton ?
|
|
777
1286
|
<ReqoreButton
|
|
778
1287
|
transparent
|
|
779
1288
|
icon='MoneyDollarCircleLine'
|
|
@@ -786,19 +1295,19 @@ export const TemplateField = memo(
|
|
|
786
1295
|
{' '}
|
|
787
1296
|
Use Template{' '}
|
|
788
1297
|
</ReqoreButton>
|
|
789
|
-
|
|
1298
|
+
: null}
|
|
790
1299
|
|
|
791
|
-
{size(menuActions?.items) > 0 ?
|
|
1300
|
+
{size(menuActions?.items) > 0 ?
|
|
792
1301
|
<MenuActionsSection actions={menuActions} size={rest.size} />
|
|
793
|
-
|
|
1302
|
+
: null}
|
|
794
1303
|
|
|
795
|
-
{size(menuItems) > 0 ?
|
|
1304
|
+
{size(menuItems) > 0 ?
|
|
796
1305
|
<CustomMenuItems
|
|
797
1306
|
items={menuItems}
|
|
798
1307
|
setIsTemplate={setIsTemplate}
|
|
799
1308
|
setTemplateValue={setTemplateValue}
|
|
800
1309
|
/>
|
|
801
|
-
|
|
1310
|
+
: null}
|
|
802
1311
|
</ReqoreMenu>
|
|
803
1312
|
}
|
|
804
1313
|
/>
|
|
@@ -822,7 +1331,10 @@ export const TemplateField = memo(
|
|
|
822
1331
|
menuItems,
|
|
823
1332
|
menuActions,
|
|
824
1333
|
internalIsFunction,
|
|
1334
|
+
effectiveIsFunction,
|
|
825
1335
|
hasInputAffordance,
|
|
1336
|
+
// the row decides whether this field draws a menu at all
|
|
1337
|
+
rowMenu,
|
|
826
1338
|
]);
|
|
827
1339
|
|
|
828
1340
|
// When the type is a list, and it has an element type - that element type is different
|
|
@@ -831,12 +1343,12 @@ export const TemplateField = memo(
|
|
|
831
1343
|
// This is a special case only for lists with element types
|
|
832
1344
|
const componentTemplates = useMemo(
|
|
833
1345
|
() =>
|
|
834
|
-
type === 'list' && (rest.ui_element_type || rest.element_type)
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1346
|
+
type === 'list' && (rest.ui_element_type || rest.element_type) ?
|
|
1347
|
+
templates
|
|
1348
|
+
: {
|
|
1349
|
+
...filteredTemplates,
|
|
1350
|
+
...TemplatesListProps,
|
|
1351
|
+
},
|
|
840
1352
|
[JSON.stringify(filteredTemplates), rest.ui_element_type, rest.element_type, type]
|
|
841
1353
|
);
|
|
842
1354
|
|
|
@@ -854,9 +1366,14 @@ export const TemplateField = memo(
|
|
|
854
1366
|
is_expression: true,
|
|
855
1367
|
value,
|
|
856
1368
|
}}
|
|
1369
|
+
// The host's per-ui_type editors reach this field through the
|
|
1370
|
+
// rest-spread; the expression shell needs them explicitly or the
|
|
1371
|
+
// builder's operands render "Unknown type!" for any consumer
|
|
1372
|
+
// ui_type (an assertion's `test-reference` Value, for one).
|
|
1373
|
+
componentOverrides={componentOverrides}
|
|
857
1374
|
localTemplates={templates}
|
|
858
1375
|
type={type as string}
|
|
859
|
-
returnType={(returnType ||
|
|
1376
|
+
returnType={(returnType || expressionDataType) as any}
|
|
860
1377
|
onChange={handleExpressionChange}
|
|
861
1378
|
readOnly={rest.readOnly || rest.disabled}
|
|
862
1379
|
expressions={rest.expressions}
|
|
@@ -864,9 +1381,29 @@ export const TemplateField = memo(
|
|
|
864
1381
|
serverHandled={rest.server_expression_handling}
|
|
865
1382
|
extraActions={extraActions}
|
|
866
1383
|
size={rest.size}
|
|
1384
|
+
// An author who typed the expression as text is already
|
|
1385
|
+
// writing in that language — dropping them into the visual
|
|
1386
|
+
// builder would make them find their own sentence again.
|
|
1387
|
+
defaultMode={expressionFromText !== null ? 'text' : 'visual'}
|
|
867
1388
|
reorder={reorder}
|
|
868
1389
|
/>
|
|
869
1390
|
</ReqoreErrorBoundary>
|
|
1391
|
+
{expressionFromText !== null ?
|
|
1392
|
+
<ReqoreButton
|
|
1393
|
+
fixed
|
|
1394
|
+
compact
|
|
1395
|
+
minimal
|
|
1396
|
+
icon='ArrowGoBackLine'
|
|
1397
|
+
// No `intent` with `minimal`: a Reqore intent is a FILL, and
|
|
1398
|
+
// `muted` as TEXT is 1.3:1 on the app surface.
|
|
1399
|
+
size={rest.size}
|
|
1400
|
+
className='dpql-detected-undo'
|
|
1401
|
+
tooltip={`Keep "${expressionFromText}" as plain text instead`}
|
|
1402
|
+
onClick={undoExpressionFromText}
|
|
1403
|
+
>
|
|
1404
|
+
Undo
|
|
1405
|
+
</ReqoreButton>
|
|
1406
|
+
: null}
|
|
870
1407
|
{renderControls()}
|
|
871
1408
|
</ReqoreControlGroup>
|
|
872
1409
|
);
|
|
@@ -880,10 +1417,11 @@ export const TemplateField = memo(
|
|
|
880
1417
|
is_expression: true,
|
|
881
1418
|
value,
|
|
882
1419
|
}}
|
|
1420
|
+
componentOverrides={componentOverrides}
|
|
883
1421
|
localTemplates={templates}
|
|
884
1422
|
level={level}
|
|
885
1423
|
type={type as string}
|
|
886
|
-
returnType={(returnType ||
|
|
1424
|
+
returnType={(returnType || expressionDataType) as any}
|
|
887
1425
|
onChange={handleExpressionChange}
|
|
888
1426
|
readOnly={rest.readOnly || rest.disabled}
|
|
889
1427
|
expressions={rest.expressions}
|
|
@@ -907,7 +1445,15 @@ export const TemplateField = memo(
|
|
|
907
1445
|
return <ReadOnlyTemplateTag value={templateValue} templates={templates} size={rest.size} />;
|
|
908
1446
|
}
|
|
909
1447
|
|
|
910
|
-
return
|
|
1448
|
+
return (
|
|
1449
|
+
<Comp
|
|
1450
|
+
value={value}
|
|
1451
|
+
onChange={onChange}
|
|
1452
|
+
name={name}
|
|
1453
|
+
{...rest}
|
|
1454
|
+
componentOverrides={componentOverrides}
|
|
1455
|
+
/>
|
|
1456
|
+
);
|
|
911
1457
|
}
|
|
912
1458
|
|
|
913
1459
|
return (
|
|
@@ -919,7 +1465,7 @@ export const TemplateField = memo(
|
|
|
919
1465
|
stack={false}
|
|
920
1466
|
verticalAlign='flex-start'
|
|
921
1467
|
>
|
|
922
|
-
{!isTemplate && allowCustomValues ?
|
|
1468
|
+
{!isTemplate && allowCustomValues ?
|
|
923
1469
|
<Component
|
|
924
1470
|
value={value}
|
|
925
1471
|
allowTemplates={allowTemplates}
|
|
@@ -927,32 +1473,61 @@ export const TemplateField = memo(
|
|
|
927
1473
|
name={name}
|
|
928
1474
|
level={level}
|
|
929
1475
|
{...rest}
|
|
1476
|
+
componentOverrides={componentOverrides}
|
|
930
1477
|
{...componentTypeProp}
|
|
931
1478
|
aria-label={fieldAriaLabel}
|
|
932
1479
|
className={`${className} template-selector`}
|
|
933
1480
|
templates={componentTemplates}
|
|
934
1481
|
/>
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
{
|
|
938
|
-
|
|
1482
|
+
: null}
|
|
1483
|
+
|
|
1484
|
+
{/* Template mode's editor for a value that can also be typed: each
|
|
1485
|
+
reference in it is a chip named as the catalogue names it, while the
|
|
1486
|
+
field still stores the plain string. A textarea spelled a chosen
|
|
1487
|
+
template `$local:name` wherever it was edited — in the Visual
|
|
1488
|
+
builder's operands, in every string field that takes templates. */}
|
|
1489
|
+
{isTemplate && templateSupportsCustomValues && !templateValueIsBracedToken ?
|
|
1490
|
+
<RichTextFormField
|
|
939
1491
|
className='template-selector'
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
onChange={handleTemplateFieldChange}
|
|
949
|
-
{...rest}
|
|
1492
|
+
valueFormat='text'
|
|
1493
|
+
singleLine={isSingleLineStringType('string')}
|
|
1494
|
+
value={typeof templateValue === 'string' ? templateValue : ''}
|
|
1495
|
+
templates={filteredTemplates}
|
|
1496
|
+
allowTemplates
|
|
1497
|
+
onChange={handleTemplateTextChange}
|
|
1498
|
+
// `tags` here are the field's own chips; the editor's `tags` are its template list.
|
|
1499
|
+
{...omit(rest, 'tags')}
|
|
950
1500
|
aria-label={fieldAriaLabel}
|
|
951
1501
|
/>
|
|
952
|
-
|
|
1502
|
+
: null}
|
|
953
1503
|
|
|
954
|
-
{
|
|
955
|
-
|
|
1504
|
+
{dpqlOffer ?
|
|
1505
|
+
<ReqoreControlGroup fixed stack size={rest.size}>
|
|
1506
|
+
<ReqoreButton
|
|
1507
|
+
compact
|
|
1508
|
+
icon='Functions'
|
|
1509
|
+
intent='info'
|
|
1510
|
+
className='dpql-detected-offer'
|
|
1511
|
+
tooltip={`"${dpqlOffer.text}" reads as an expression. It is also valid text here, so nothing has been changed — use it as an expression?`}
|
|
1512
|
+
onClick={acceptDpqlOffer}
|
|
1513
|
+
>
|
|
1514
|
+
Use as expression
|
|
1515
|
+
</ReqoreButton>
|
|
1516
|
+
<ReqoreButton
|
|
1517
|
+
compact
|
|
1518
|
+
icon='CloseLine'
|
|
1519
|
+
intent='info'
|
|
1520
|
+
className='dpql-detected-dismiss'
|
|
1521
|
+
tooltip='Keep it as plain text'
|
|
1522
|
+
onClick={declineDpqlOffer}
|
|
1523
|
+
/>
|
|
1524
|
+
</ReqoreControlGroup>
|
|
1525
|
+
: null}
|
|
1526
|
+
|
|
1527
|
+
{(
|
|
1528
|
+
showTemplatesDropdown ||
|
|
1529
|
+
(isTemplate && templateSupportsCustomValues && templateValueIsBracedToken)
|
|
1530
|
+
) ?
|
|
956
1531
|
<TemplateDropdownSelector
|
|
957
1532
|
allowCustomValues={allowCustomValues}
|
|
958
1533
|
templates={templates}
|
|
@@ -964,10 +1539,27 @@ export const TemplateField = memo(
|
|
|
964
1539
|
label={label}
|
|
965
1540
|
hasOnlyAllowedValues={hasOnlyAllowedValues}
|
|
966
1541
|
/>
|
|
967
|
-
|
|
1542
|
+
: null}
|
|
968
1543
|
|
|
969
1544
|
{renderControls()}
|
|
970
1545
|
</ReqoreControlGroup>
|
|
971
1546
|
);
|
|
972
1547
|
}
|
|
973
1548
|
);
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* The row channel belongs to the row's OWN editor: this field. It is read here
|
|
1552
|
+
* and hidden from everything this field renders, so a field nested inside it —
|
|
1553
|
+
* an expression operand, a list item — keeps drawing its own menu instead of
|
|
1554
|
+
* publishing actions into a row menu that cannot say which value they act on.
|
|
1555
|
+
* `undefined` outside a row, where this field draws its own menu too.
|
|
1556
|
+
*/
|
|
1557
|
+
export const TemplateField = memo((props: ITemplateFieldProps) => {
|
|
1558
|
+
const rowMenu = useRowMenu();
|
|
1559
|
+
|
|
1560
|
+
return (
|
|
1561
|
+
<RowMenuContext.Provider value={undefined}>
|
|
1562
|
+
<TemplateFieldImpl {...props} rowMenu={rowMenu} />
|
|
1563
|
+
</RowMenuContext.Provider>
|
|
1564
|
+
);
|
|
1565
|
+
});
|