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