@qoretechnologies/reqraft 0.10.61 → 0.10.62-pr.137.g86641aa

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.
Files changed (101) hide show
  1. package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
  2. package/dist/components/dpqlEditor/DpqlEditor.js +8 -3
  3. package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
  4. package/dist/components/dpqlEditor/types.d.ts +5 -0
  5. package/dist/components/dpqlEditor/types.d.ts.map +1 -1
  6. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  7. package/dist/components/form/engine/CompactRow.js +17 -8
  8. package/dist/components/form/engine/CompactRow.js.map +1 -1
  9. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  10. package/dist/components/form/engine/FormEngine.js +65 -15
  11. package/dist/components/form/engine/FormEngine.js.map +1 -1
  12. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  13. package/dist/components/form/engine/OptionFieldMessages.js +12 -1
  14. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  15. package/dist/components/form/engine/compactRowContext.d.ts +2 -0
  16. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  17. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  18. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  19. package/dist/components/form/engine/readFirst.js +5 -1
  20. package/dist/components/form/engine/readFirst.js.map +1 -1
  21. package/dist/components/form/expressions/ExpressionField.d.ts +21 -1
  22. package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
  23. package/dist/components/form/expressions/ExpressionField.js +72 -14
  24. package/dist/components/form/expressions/ExpressionField.js.map +1 -1
  25. package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
  26. package/dist/components/form/expressions/builder/index.js +16 -3
  27. package/dist/components/form/expressions/builder/index.js.map +1 -1
  28. package/dist/components/form/expressions/expressionOperands.d.ts +16 -0
  29. package/dist/components/form/expressions/expressionOperands.d.ts.map +1 -0
  30. package/dist/components/form/expressions/expressionOperands.js +97 -0
  31. package/dist/components/form/expressions/expressionOperands.js.map +1 -0
  32. package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
  33. package/dist/components/form/expressions/mockExpressions.js +15 -0
  34. package/dist/components/form/expressions/mockExpressions.js.map +1 -1
  35. package/dist/components/form/expressions/renderExpressionToText.d.ts +2 -0
  36. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  37. package/dist/components/form/expressions/renderExpressionToText.js +24 -5
  38. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  39. package/dist/components/form/expressions/textOfExpression.d.ts +18 -0
  40. package/dist/components/form/expressions/textOfExpression.d.ts.map +1 -0
  41. package/dist/components/form/expressions/textOfExpression.js +37 -0
  42. package/dist/components/form/expressions/textOfExpression.js.map +1 -0
  43. package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
  44. package/dist/components/form/expressions/useRenderExpression.js +7 -7
  45. package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
  46. package/dist/components/form/fields/rich-text/RichText.d.ts +8 -1
  47. package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
  48. package/dist/components/form/fields/rich-text/RichText.js +10 -10
  49. package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
  50. package/dist/components/form/fields/template/TemplateField.d.ts +6 -0
  51. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  52. package/dist/components/form/fields/template/TemplateField.js +280 -55
  53. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  54. package/dist/components/form/fields/template/writtenAsText.d.ts +57 -0
  55. package/dist/components/form/fields/template/writtenAsText.d.ts.map +1 -0
  56. package/dist/components/form/fields/template/writtenAsText.js +203 -0
  57. package/dist/components/form/fields/template/writtenAsText.js.map +1 -0
  58. package/dist/components/form/index.d.ts +2 -0
  59. package/dist/components/form/index.d.ts.map +1 -1
  60. package/dist/components/form/index.js +7 -1
  61. package/dist/components/form/index.js.map +1 -1
  62. package/dist/components/smartEditor/useLspSession.d.ts +6 -0
  63. package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
  64. package/dist/components/smartEditor/useLspSession.js +7 -6
  65. package/dist/components/smartEditor/useLspSession.js.map +1 -1
  66. package/dist/helpers/templates.d.ts +18 -0
  67. package/dist/helpers/templates.d.ts.map +1 -1
  68. package/dist/helpers/templates.js +30 -1
  69. package/dist/helpers/templates.js.map +1 -1
  70. package/dist/helpers/validations.d.ts.map +1 -1
  71. package/dist/helpers/validations.js +98 -18
  72. package/dist/helpers/validations.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/components/dpqlEditor/DpqlEditor.tsx +11 -18
  75. package/src/components/dpqlEditor/types.ts +5 -0
  76. package/src/components/form/engine/CompactRow.tsx +31 -10
  77. package/src/components/form/engine/FormEngine.stories.tsx +656 -18
  78. package/src/components/form/engine/FormEngine.tsx +64 -22
  79. package/src/components/form/engine/FormEngineRemote.stories.tsx +5 -0
  80. package/src/components/form/engine/OptionFieldMessages.tsx +12 -1
  81. package/src/components/form/engine/compactRowContext.ts +2 -0
  82. package/src/components/form/engine/readFirst.ts +7 -1
  83. package/src/components/form/expressions/ExpressionField.stories.tsx +57 -9
  84. package/src/components/form/expressions/ExpressionField.tsx +179 -60
  85. package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +8 -1
  86. package/src/components/form/expressions/builder/index.tsx +16 -3
  87. package/src/components/form/expressions/dpqlMockLanguage.ts +7 -2
  88. package/src/components/form/expressions/expressionOperands.ts +74 -0
  89. package/src/components/form/expressions/mockExpressions.ts +16 -4
  90. package/src/components/form/expressions/renderExpressionToText.ts +26 -9
  91. package/src/components/form/expressions/textOfExpression.ts +39 -0
  92. package/src/components/form/expressions/useRenderExpression.ts +9 -1
  93. package/src/components/form/fields/number/Number.stories.tsx +8 -7
  94. package/src/components/form/fields/rich-text/RichText.tsx +18 -10
  95. package/src/components/form/fields/template/TemplateField.stories.tsx +486 -18
  96. package/src/components/form/fields/template/TemplateField.tsx +506 -62
  97. package/src/components/form/fields/template/writtenAsText.ts +217 -0
  98. package/src/components/form/index.tsx +6 -0
  99. package/src/components/smartEditor/useLspSession.ts +15 -10
  100. package/src/helpers/templates.ts +35 -0
  101. package/src/helpers/validations.ts +83 -12
@@ -35,7 +35,7 @@ import {
35
35
  filterTemplatesByType as templatesFilterFunc,
36
36
  getTemplateKey,
37
37
  getTemplateValue,
38
- isBracedTemplateToken,
38
+ findTemplate,
39
39
  isCompleteTemplateToken,
40
40
  describeTemplateReference,
41
41
  isValueTemplate,
@@ -67,6 +67,20 @@ import { RichTextFormField } from '../rich-text/RichText';
67
67
  import { richtextToString } from '../../../../helpers/common';
68
68
  import { isSingleLineStringType } from '../../../../helpers/singleLineString';
69
69
  import { isUntypedOptionType } from '../../../../helpers/optionUiTypes';
70
+ import {
71
+ expressionSeedOf,
72
+ isWrittenAsText,
73
+ isWrittenAsTextOnTheValueTab,
74
+ loneTemplateOf,
75
+ loneValueOf,
76
+ expressionTextOfValue,
77
+ sameValueType,
78
+ templateTypeName,
79
+ valueTextOf,
80
+ TValueTab,
81
+ templateTextValue,
82
+ untypedTextOf,
83
+ } from './writtenAsText';
70
84
  import {
71
85
  IRowMenuRegistration,
72
86
  RowMenuContext,
@@ -183,6 +197,12 @@ export interface ITemplateFieldProps extends Partial<
183
197
  * arguments, array items) never receive it and stay IDE-verbatim.
184
198
  */
185
199
  allowTextExpressions?: boolean;
200
+ /**
201
+ * The value is entered on tabs (qorus#646, David): Value · Expression · Visual where the field takes
202
+ * templates and expressions written as text, Value · Template where it takes templates only. A form's
203
+ * options set it (FormEngine); an expression's operands and other hosts keep the field as it was.
204
+ */
205
+ valueTabs?: boolean;
186
206
  /**
187
207
  * SEAM (reqraft): host-injected per-card actions for the expression editor
188
208
  * this field renders in expression mode (the IDE's AI-assist button).
@@ -525,6 +545,7 @@ const TemplateFieldImpl = memo(
525
545
  allowTemplates = true,
526
546
  allowFunctions,
527
547
  allowTextExpressions,
548
+ valueTabs,
528
549
  extraActions,
529
550
  componentOverrides,
530
551
  allowCustomValues = true,
@@ -663,15 +684,55 @@ const TemplateFieldImpl = memo(
663
684
  const emptyLandsOnTemplates =
664
685
  typeIsAnyLike && (hasTemplatesOnOffer || (!!allowCustomValues && !hasOnlyAllowedValues));
665
686
  const opensOnTemplates = isEmptyValue && emptyLandsOnTemplates;
687
+ /* An untyped field holding null is written in the same text field, as `null`: the editor an untyped
688
+ field otherwise falls to drew null as an empty box, the same as a field holding nothing, so the
689
+ author could not tell the two apart (qorus#646). */
690
+ const nullWrittenAsText =
691
+ value === null && typeIsAnyLike && !!allowCustomValues && !hasOnlyAllowedValues;
692
+
693
+ /* The tabs a value is entered on (qorus#646, David): Value · Expression · Visual where the field takes
694
+ templates and expressions written as text, Value · Template where it takes templates only, none where
695
+ it takes no templates. One value, three views: see `selectTab`. */
696
+ const valueTabIsText = isWrittenAsTextOnTheValueTab(type as string) && !hasOnlyAllowedValues;
697
+ const tabsKind: 'full' | 'template' | undefined =
698
+ !valueTabs || !allowTemplates || editorHandlesTemplates || rest.arg_schema ? undefined
699
+ : allowFunctions && allowTextExpressions && !hasOnlyAllowedValues ? 'full'
700
+ : 'template';
701
+ /** The value is an expression (its AST), as the host says. */
702
+ const valueIsExpression =
703
+ !!(isFunction || isDefaultFunction) && value !== null && typeof value === 'object';
704
+ /** Where a saved value opens: an expression on Expression; a template on Value where it is written as
705
+ * text, else on Expression (or Template, for a template-only field); anything else on Value. */
706
+ const [tab, setTab] = useState<TValueTab>(() => {
707
+ if (!tabsKind) return 'value';
708
+ if (valueIsExpression) return tabsKind === 'full' ? 'expression' : 'value';
709
+ if (typeof value === 'string' && isValueTemplate(value) && !valueTabIsText) {
710
+ return tabsKind === 'full' ? 'expression' : 'template';
711
+ }
712
+ return 'value';
713
+ });
714
+ const opensInExpressionText = tabsKind === 'full' && tab === 'expression' && !valueIsExpression;
715
+ const tabsKindRef = useRef(tabsKind);
716
+ tabsKindRef.current = tabsKind;
717
+ /** What the Text view is seeded with when the value is not an expression yet; null outside it. */
718
+ const [expressionSeed, setExpressionSeed] = useState<string | null>(
719
+ opensInExpressionText ? expressionTextOfValue(value) : null
720
+ );
666
721
 
667
722
  const [isTemplateState, setIsTemplate] = useState<boolean>(
668
- (isDefaultTemplate || isValueTemplate(value) || !allowCustomValues || opensOnTemplates) &&
669
- allowTemplates
723
+ tabsKind ?
724
+ tab === 'template' || (tab === 'value' && valueTabIsText)
725
+ : (isDefaultTemplate ||
726
+ isValueTemplate(value) ||
727
+ !allowCustomValues ||
728
+ opensOnTemplates ||
729
+ nullWrittenAsText) &&
730
+ !!allowTemplates
670
731
  );
671
732
 
672
733
  const isTemplate = editorHandlesTemplates ? false : isTemplateState;
673
734
  const [internalIsFunction, setInternalIsFunction] = useState<boolean>(
674
- !!isDefaultFunction && !!allowFunctions
735
+ tabsKind ? tab === 'expression' || tab === 'visual' : !!isDefaultFunction && !!allowFunctions
675
736
  );
676
737
  const [templateValue, setTemplateValue] = useState<string | null>(value);
677
738
 
@@ -701,6 +762,12 @@ const TemplateFieldImpl = memo(
701
762
  if (isTemplate && isValueTemplate(value)) {
702
763
  setTemplateValue(value);
703
764
  }
765
+ /* Emptied from outside the editor - the card's "Clear value", an undo, a host's reset - the text the
766
+ value is written in is emptied too. It kept its own copy, so a cleared value went on showing what it
767
+ had held (qorus#646: "Clear value" had no effect). The editor's own emptying sets this already. */
768
+ if (isEmptyValue && templateValue) {
769
+ setTemplateValue(null);
770
+ }
704
771
  }, [JSON.stringify(value)]);
705
772
 
706
773
  /* Clearing an untyped field returns it to the template selector, which is
@@ -766,8 +833,22 @@ const TemplateFieldImpl = memo(
766
833
  }
767
834
  }, [allowCustomValues]);
768
835
 
836
+ // null arriving later (written in the Text tab, say) is written in the text field too - see above
769
837
  useEffect(() => {
770
- if (!isTemplate && isValueTemplate(value) && allowTemplates) {
838
+ if (!isTemplate && nullWrittenAsText && allowTemplates) {
839
+ setTemplateValue(null);
840
+ setIsTemplate(true);
841
+ }
842
+ }, [nullWrittenAsText, allowTemplates]);
843
+
844
+ useEffect(() => {
845
+ // a template on the Expression tab stays there: it is the expression editor's to show
846
+ if (
847
+ !isTemplate &&
848
+ isValueTemplate(value) &&
849
+ allowTemplates &&
850
+ !(tabsKind && internalIsFunction)
851
+ ) {
771
852
  // In auto mode, leave user-typed dollar-strings alone ('$foo: hello'
772
853
  // passes the loose check) — but a string that IS one well-formed token
773
854
  // ($data:{…}, $config:item) must still flip into template mode, or an
@@ -799,6 +880,13 @@ const TemplateFieldImpl = memo(
799
880
  // When template key or template value change run the onChange function
800
881
  useUpdateEffect(() => {
801
882
  if (templateValue) {
883
+ /* Read from the text where the field is written as text: a lone template takes its own
884
+ type in an untyped field, a scalar's literal is that literal, other text is text. */
885
+ if (templateSupportsCustomValues) {
886
+ const read = templateTextValue(templateValue, type as string, filteredTemplates);
887
+ onChange?.(name, read.value, read.type as TQorusType, effectiveIsFunction);
888
+ return;
889
+ }
802
890
  onChange?.(name, templateValue, type as TQorusType, effectiveIsFunction);
803
891
  }
804
892
  }, [JSON.stringify(templateValue)]);
@@ -808,15 +896,6 @@ const TemplateFieldImpl = memo(
808
896
  const showTemplateToggle =
809
897
  allowCustomValues && allowTemplates && !rest.arg_schema && !editorHandlesTemplates;
810
898
 
811
- // Only a BRACED context ref (`$data:{…}` — machine-written, nobody types
812
- // one) renders as the picker chip (named via `resolveTemplateLabel`)
813
- // rather than as its raw text in a string editor. Plain word-path tokens
814
- // (`$local:id`) are typeable, so per the build #123 review they keep the
815
- // input that offers templates while typing. Mixed text-and-token strings
816
- // keep the string editor too; the chip-in-editor treatment arrives with
817
- // the rich-text string mode.
818
- const templateValueIsBracedToken = isBracedTemplateToken(templateValue);
819
-
820
899
  /* An UNTYPED field can be typed into as well as picked from.
821
900
 
822
901
  Template mode has a typable editor whenever the field's type can hold
@@ -832,11 +911,19 @@ const TemplateFieldImpl = memo(
832
911
  specific to that field: every empty untyped field with templates on
833
912
  offer got the same downgrade. The editor still offers the same templates
834
913
  on focus, so nothing is lost by being able to type as well. */
914
+ /* And so can a scalar: a whole number, a number or a yes/no is written as text too
915
+ (qorus#646). A template chosen into it is a chip in that text, not a pick-only
916
+ control that holds the template and nothing else, so text can be written around it;
917
+ what the field then holds is read from the text (writtenAsText). A date keeps its
918
+ date control. */
835
919
  const templateSupportsCustomValues =
836
- allowCustomValues && (type === 'string' || typeIsAnyLike) && !hasOnlyAllowedValues;
920
+ allowCustomValues && isWrittenAsText(type as string) && !hasOnlyAllowedValues;
837
921
  const showTemplatesDropdown =
838
922
  allowTemplates && (!allowCustomValues || (isTemplate && !templateSupportsCustomValues));
839
923
  const hasOnlyExpressions = !allowCustomValues && !allowTemplates && allowFunctions;
924
+ /** A date field with templates on offer: they are offered beside its date control. */
925
+ const dateTakesTemplates =
926
+ type === 'date' && !!allowTemplates && hasTemplatesOnOffer && !hasOnlyAllowedValues;
840
927
  // True when some input control renders besides the ⋮ menu. When nothing
841
928
  // does (an empty `any` field: custom values are disallowed and the value's
842
929
  // type is picked FROM the menu), the menu trigger is the field's only
@@ -860,8 +947,11 @@ const TemplateFieldImpl = memo(
860
947
  /* Emptying the text leaves the field where an empty field lands. For
861
948
  an untyped field that is THIS editor: switching to custom mode here
862
949
  mounted a different editor in its place, so deleting the last
863
- character lost the cursor. */
864
- if (!(emptyLandsOnTemplates && allowTemplates)) {
950
+ character lost the cursor. A field written as text (a whole number, a
951
+ number, a yes/no) is the same: deleting the template in it left a
952
+ different, empty editor in its place, and what was typed next went
953
+ nowhere. */
954
+ if (!((emptyLandsOnTemplates || templateSupportsCustomValues) && allowTemplates)) {
865
955
  setIsTemplate(false);
866
956
  }
867
957
  setTemplateValue(null);
@@ -870,7 +960,7 @@ const TemplateFieldImpl = memo(
870
960
  setTemplateValue(val);
871
961
  }
872
962
  },
873
- [name, onChange, emptyLandsOnTemplates, allowTemplates]
963
+ [name, onChange, emptyLandsOnTemplates, templateSupportsCustomValues, allowTemplates]
874
964
  );
875
965
 
876
966
  const handleTemplateTextChange = useCallback(
@@ -923,6 +1013,15 @@ const TemplateFieldImpl = memo(
923
1013
  // `SaveValueButton` in the controls menu — the saved-values storage is
924
1014
  // IDE-only, so the menu item is dropped (`allowSaving` is inert).
925
1015
 
1016
+ /* A template chosen beside a date control is the value at once, IN the template control: handed to
1017
+ the form while the field was still drawing its date control, the reference reached the date
1018
+ picker first, which threw on it ("Invalid ISO 8601 date time string"). */
1019
+ const handleSelectDateTemplate = useCallback((item: { value?: unknown }) => {
1020
+ if (typeof item?.value !== 'string') return;
1021
+ setIsTemplate(true);
1022
+ setTemplateValue(item.value);
1023
+ }, []);
1024
+
926
1025
  const handleRemoveTemplateClick = useCallback(() => {
927
1026
  /* This is the `×` ON the template control, and it means "I do not want a
928
1027
  template here" — on a field whose menu offers no `Set Custom Value` it
@@ -990,6 +1089,46 @@ const TemplateFieldImpl = memo(
990
1089
  }
991
1090
  }, [name, onChange, value, templateValue]);
992
1091
 
1092
+ /* One value, three views (qorus#646, David). Value → Expression writes the literal or template as
1093
+ expression text, emitting nothing until it is edited. Expression ↔ Visual is the editor's own toggle.
1094
+ Expression → Value converts only a literal or a lone template; any other expression stays, and the
1095
+ Value tab says it cannot show it and offers to replace it. Nothing is lost silently. */
1096
+ const selectTab = useCallback(
1097
+ (next: TValueTab) => {
1098
+ if (next === tab) return;
1099
+ if (next === 'value' || next === 'template') {
1100
+ if (valueIsExpression) {
1101
+ const envelope = { is_expression: true, value };
1102
+ const template = loneTemplateOf(envelope);
1103
+ const literal = template === undefined ? loneValueOf(envelope) : undefined;
1104
+ if (template !== undefined) {
1105
+ setTemplateValue(template);
1106
+ onChange?.(name, template, type as TQorusType, false);
1107
+ } else if (literal !== undefined) {
1108
+ setTemplateValue(valueTextOf(literal.value, type as string) || null);
1109
+ onChange?.(name, literal.value, type as TQorusType, false);
1110
+ }
1111
+ }
1112
+ setInternalIsFunction(false);
1113
+ setExpressionFromText(null);
1114
+ setIsTemplate(next === 'template' || valueTabIsText);
1115
+ } else {
1116
+ if (!valueIsExpression) setExpressionSeed(expressionTextOfValue(value));
1117
+ setInternalIsFunction(true);
1118
+ }
1119
+ setTab(next);
1120
+ },
1121
+ [tab, valueIsExpression, value, name, onChange, type, valueTabIsText]
1122
+ );
1123
+
1124
+ /** Replace what a tab cannot show with an empty value of its own kind. */
1125
+ const replaceWithAValue = useCallback(() => {
1126
+ setInternalIsFunction(false);
1127
+ setTemplateValue(null);
1128
+ setExpressionSeed(null);
1129
+ onChange?.(name, undefined, type as TQorusType, false);
1130
+ }, [name, onChange, type]);
1131
+
993
1132
  const handleTemplateToggleClick = useCallback(() => {
994
1133
  setInternalIsFunction(false);
995
1134
  onChange(name, undefined, undefined, false);
@@ -1001,12 +1140,30 @@ const TemplateFieldImpl = memo(
1001
1140
  (expressionValue: IExpression | undefined, remove: boolean) => {
1002
1141
  if (remove) {
1003
1142
  setInternalIsFunction(false);
1143
+ setExpressionSeed(null);
1004
1144
  // The expression is gone, so there is no longer a switch to undo.
1005
1145
  setExpressionFromText(null);
1006
1146
  }
1147
+ /* On the Expression tab, a lone template is the template: stored bare, as every consumer
1148
+ evaluates it (see `loneTemplateOf`) - the server evaluates its expression form, template(...),
1149
+ to the template's parts, not to its value. The Text view stays, seeded with it. */
1150
+ const onTabs = !!tabsKindRef.current;
1151
+ const lone = remove || !onTabs ? undefined : loneTemplateOf(expressionValue);
1152
+ if (lone !== undefined) {
1153
+ setExpressionSeed(lone);
1154
+ onChange(name, lone, type as TQorusType, false);
1155
+ return;
1156
+ }
1157
+ // and a lone value (12, true) is that value, the field's own shape (see `loneValueOf`)
1158
+ const literal = remove || !onTabs ? undefined : loneValueOf(expressionValue);
1159
+ if (literal !== undefined) {
1160
+ setExpressionSeed(expressionSeedOf(literal.value));
1161
+ onChange(name, literal.value, type as TQorusType, false);
1162
+ return;
1163
+ }
1007
1164
  onChange(name, expressionValue?.value, expressionDataType as TQorusType, !remove);
1008
1165
  },
1009
- [name, onChange, expressionDataType, value]
1166
+ [name, onChange, expressionDataType, value, type]
1010
1167
  );
1011
1168
 
1012
1169
  // ─── Text typed into a plain field that is really a DPQL expression ───
@@ -1046,6 +1203,8 @@ const TemplateFieldImpl = memo(
1046
1203
  setInternalIsFunction(true);
1047
1204
  setIsTemplate(false);
1048
1205
  setTemplateValue(null);
1206
+ // the Value tab never holds an expression: detected, it moves to the Expression tab, with an undo
1207
+ if (tabsKindRef.current === 'full') setTab('expression');
1049
1208
  // `dpql/parse` answers with the field-ready envelope
1050
1209
  // (`{is_expression, value}`); this field stores the inner AST and
1051
1210
  // signals the flag through `onChange`'s fourth argument, exactly as
@@ -1062,6 +1221,10 @@ const TemplateFieldImpl = memo(
1062
1221
 
1063
1222
  declinedTexts.current.add(expressionFromText);
1064
1223
  setInternalIsFunction(false);
1224
+ if (tabsKind) {
1225
+ setTab('value');
1226
+ setIsTemplate(valueTabIsText);
1227
+ }
1065
1228
  setExpressionFromText(null);
1066
1229
  onChange?.(name, expressionFromText, type as TQorusType, false);
1067
1230
  }, [expressionFromText, name, onChange, type]);
@@ -1097,7 +1260,8 @@ const TemplateFieldImpl = memo(
1097
1260
  !!allowFunctions &&
1098
1261
  !!allowTextExpressions &&
1099
1262
  !effectiveIsFunction &&
1100
- !isTemplate &&
1263
+ // the template editor is text too: an expression written around a template is detected there
1264
+ (!isTemplate || templateSupportsCustomValues) &&
1101
1265
  !hasOnlyAllowedValues &&
1102
1266
  !rest.readonly &&
1103
1267
  !rest.readOnly &&
@@ -1149,7 +1313,8 @@ const TemplateFieldImpl = memo(
1149
1313
 
1150
1314
  const outcome = classifyTypedText({ text, type, field: validationField, parsed });
1151
1315
 
1152
- if (outcome === 'switch') {
1316
+ // on tabs, an expression detected switches (with an undo) rather than being offered
1317
+ if (outcome === 'switch' || (outcome === 'offer' && tabsKindRef.current === 'full')) {
1153
1318
  enterExpressionRef.current(text, parsed.expression);
1154
1319
  } else if (outcome === 'offer') {
1155
1320
  setDpqlOffer({ text, expression: parsed.expression });
@@ -1188,36 +1353,41 @@ const TemplateFieldImpl = memo(
1188
1353
 
1189
1354
  const publishedItems = useMemo(
1190
1355
  () => [
1191
- ...(canOfferExpression && !functions.loading
1192
- ? [
1193
- {
1194
- label: 'Use Expression',
1195
- icon: 'Functions' as const,
1196
- tooltip: 'Run a function on this value',
1197
- onClick: handleSelectFunctionChange,
1198
- },
1199
- ]
1200
- : []),
1201
- ...(canOfferTemplate
1202
- ? [
1203
- {
1204
- label: 'Use Template',
1205
- icon: 'MoneyDollarCircleLine' as const,
1206
- tooltip: 'Use a template',
1207
- onClick: handleTemplateToggleClick,
1208
- },
1209
- ]
1210
- : []),
1211
- ...(canOfferCustomValue
1212
- ? [
1213
- {
1214
- label: 'Use Custom Value',
1215
- icon: 'EditLine' as const,
1216
- tooltip: 'Write the value here instead of choosing a template',
1217
- onClick: handleUseCustomValueClick,
1218
- },
1219
- ]
1220
- : []),
1356
+ // on tabs, the menu keeps value actions only: the tabs are the mode switch
1357
+ ...(tabsKind ?
1358
+ []
1359
+ : [
1360
+ ...(canOfferExpression && !functions.loading ?
1361
+ [
1362
+ {
1363
+ label: 'Use Expression',
1364
+ icon: 'Functions' as const,
1365
+ tooltip: 'Run a function on this value',
1366
+ onClick: handleSelectFunctionChange,
1367
+ },
1368
+ ]
1369
+ : []),
1370
+ ...(canOfferTemplate ?
1371
+ [
1372
+ {
1373
+ label: 'Use Template',
1374
+ icon: 'MoneyDollarCircleLine' as const,
1375
+ tooltip: 'Use a template',
1376
+ onClick: handleTemplateToggleClick,
1377
+ },
1378
+ ]
1379
+ : []),
1380
+ ]),
1381
+ ...(canOfferCustomValue && !tabsKind ?
1382
+ [
1383
+ {
1384
+ label: 'Use Custom Value',
1385
+ icon: 'EditLine' as const,
1386
+ tooltip: 'Write the value here instead of choosing a template',
1387
+ onClick: handleUseCustomValueClick,
1388
+ },
1389
+ ]
1390
+ : []),
1221
1391
  /* The "set a value of this type" choices an untyped field offers.
1222
1392
  Dividers are dropped: they grouped items in a menu this field drew
1223
1393
  itself, and in the row's shared menu they would divide other
@@ -1259,14 +1429,23 @@ const TemplateFieldImpl = memo(
1259
1429
  useRowMenuPublisher(rowMenu, publishedKey, publishedItems as never);
1260
1430
 
1261
1431
  const renderControls = useCallback(() => {
1432
+ // on tabs, the tabs are the mode switch: the menu keeps value actions only
1262
1433
  const showFunctionsDropdown =
1263
- allowFunctions && !hasOnlyAllowedValues && !rest.readonly && !internalIsFunction;
1264
- const showTemplatesButton = showTemplateToggle && !isTemplate;
1434
+ !tabsKind &&
1435
+ allowFunctions &&
1436
+ !hasOnlyAllowedValues &&
1437
+ !rest.readonly &&
1438
+ !internalIsFunction;
1439
+ const showTemplatesButton = !tabsKind && showTemplateToggle && !isTemplate;
1265
1440
  // The way back out of template mode, where the editor draws no `×`.
1266
- const showCustomValueButton = showTemplateToggle && isTemplate && templateSupportsCustomValues;
1441
+ const showCustomValueButton =
1442
+ !tabsKind && showTemplateToggle && isTemplate && templateSupportsCustomValues;
1267
1443
  // The "Set value" label promises a way to set one — reorder rows alone don't.
1268
1444
  const hasValueRows =
1269
- showFunctionsDropdown || showTemplatesButton || showCustomValueButton || size(menuItems) > 0;
1445
+ showFunctionsDropdown ||
1446
+ showTemplatesButton ||
1447
+ showCustomValueButton ||
1448
+ size(menuItems) > 0;
1270
1449
 
1271
1450
  /* A lone group opens itself. Two of the menu's groups are collapsed
1272
1451
  sections, so a menu holding nothing but one of them asked for a click
@@ -1394,7 +1573,7 @@ const TemplateFieldImpl = memo(
1394
1573
 
1395
1574
  : null}
1396
1575
 
1397
- {showTemplatesButton ?
1576
+ {showTemplatesButton && !(showFunctionsDropdown && allowTextExpressions) ?
1398
1577
  <ReqoreButton
1399
1578
  transparent
1400
1579
  icon='MoneyDollarCircleLine'
@@ -1457,6 +1636,7 @@ const TemplateFieldImpl = memo(
1457
1636
  handleSelectFunctionChange,
1458
1637
  handleTemplateToggleClick,
1459
1638
  handleUseCustomValueClick,
1639
+ allowTextExpressions,
1460
1640
  hasOnlyAllowedValues,
1461
1641
  isTemplate,
1462
1642
  rest.readonly,
@@ -1475,6 +1655,7 @@ const TemplateFieldImpl = memo(
1475
1655
  hasInputAffordance,
1476
1656
  // the row decides whether this field draws a menu at all
1477
1657
  rowMenu,
1658
+ tabsKind,
1478
1659
  ]);
1479
1660
 
1480
1661
  // When the type is a list, and it has an element type - that element type is different
@@ -1503,6 +1684,228 @@ const TemplateFieldImpl = memo(
1503
1684
  type,
1504
1685
  ]);
1505
1686
 
1687
+ // ─── Value · Expression · Visual (or Value · Template) ───────────────────────────────────────────
1688
+ if (tabsKind && !rest.disabled) {
1689
+ const tabButton = (
1690
+ key: TValueTab,
1691
+ label: string,
1692
+ icon: IReqoreIconName,
1693
+ tooltip?: string
1694
+ ) => (
1695
+ <ReqoreButton
1696
+ key={key}
1697
+ compact
1698
+ size={rest.size}
1699
+ icon={icon}
1700
+ active={tab === key}
1701
+ aria-pressed={tab === key}
1702
+ data-tab={label}
1703
+ className={`value-tab value-tab-${key}`}
1704
+ tooltip={tooltip}
1705
+ disabled={rest.readOnly || rest.readonly}
1706
+ onClick={() => selectTab(key)}
1707
+ >
1708
+ {label}
1709
+ </ReqoreButton>
1710
+ );
1711
+ /* A lone template of another type than the field's is converted when the value is used: said as a
1712
+ warning, not a block - whether it converts depends on the data (qorus#646, David). */
1713
+ const loneTemplate =
1714
+ typeof value === 'string' && isCompleteTemplateToken(value) ? value : undefined;
1715
+ const templateBadge =
1716
+ loneTemplate && !typeIsAnyLike ?
1717
+ findTemplate(templates ?? {}, loneTemplate)?.badge
1718
+ : undefined;
1719
+ const conversionNote =
1720
+ (
1721
+ typeof templateBadge === 'string' &&
1722
+ templateBadge &&
1723
+ !sameValueType(templateBadge, type as string)
1724
+ ) ?
1725
+ `${templateTypeName(templateBadge)} is converted to ${templateTypeName(type as string).toLowerCase()} when the value is used; a value that does not convert fails`
1726
+ : undefined;
1727
+ const cannotShow = (message: string) => (
1728
+ <ReqoreMessage
1729
+ size='small'
1730
+ flat
1731
+ opaque={false}
1732
+ intent='muted'
1733
+ className='value-tab-cannot-show'
1734
+ >
1735
+ {message}{' '}
1736
+ <ReqoreButton
1737
+ compact
1738
+ size={rest.size}
1739
+ className='value-tab-replace'
1740
+ onClick={replaceWithAValue}
1741
+ disabled={rest.readOnly || rest.readonly}
1742
+ >
1743
+ Replace it with a value
1744
+ </ReqoreButton>
1745
+ </ReqoreMessage>
1746
+ );
1747
+ const valueBody = () => {
1748
+ if (valueIsExpression) {
1749
+ return cannotShow("This expression can't be shown as a value.");
1750
+ }
1751
+ if (valueTabIsText) {
1752
+ return (
1753
+ <ReqoreControlGroup vertical fluid gapSize='small'>
1754
+ <RichTextFormField
1755
+ className='template-selector value-tab-text'
1756
+ valueFormat='text'
1757
+ singleLine={isSingleLineStringType('string')}
1758
+ value={
1759
+ typeof templateValue === 'string' ? templateValue : (
1760
+ valueTextOf(value, type as string)
1761
+ )
1762
+ }
1763
+ templates={filteredTemplates}
1764
+ // a chip of a field not of this type is named as the catalogue names it
1765
+ namingTemplates={templates}
1766
+ allowTemplates
1767
+ onChange={handleTemplateTextChange}
1768
+ {...rest}
1769
+ fixed={false}
1770
+ fluid
1771
+ panelProps={{
1772
+ ...(rest as { panelProps?: object }).panelProps,
1773
+ style: {
1774
+ ...(rest as { panelProps?: { style?: object } }).panelProps?.style,
1775
+ width: '150px',
1776
+ maxWidth: '100%',
1777
+ minWidth: 'fit-content',
1778
+ },
1779
+ }}
1780
+ aria-label={fieldAriaLabel}
1781
+ />
1782
+ {conversionNote ?
1783
+ <ReqoreMessage
1784
+ size='small'
1785
+ flat
1786
+ opaque={false}
1787
+ intent='warning'
1788
+ className='value-tab-conversion'
1789
+ >
1790
+ {conversionNote}
1791
+ </ReqoreMessage>
1792
+ : null}
1793
+ </ReqoreControlGroup>
1794
+ );
1795
+ }
1796
+ if (loneTemplate !== undefined || (typeof value === 'string' && isValueTemplate(value))) {
1797
+ return cannotShow(
1798
+ `A template is shown on the ${tabsKind === 'full' ? 'Expression' : 'Template'} tab, not here.`
1799
+ );
1800
+ }
1801
+ return (
1802
+ <Component
1803
+ value={value}
1804
+ allowTemplates={false}
1805
+ onChange={onChange}
1806
+ name={name}
1807
+ level={level}
1808
+ {...rest}
1809
+ componentOverrides={componentOverrides}
1810
+ {...componentTypeProp}
1811
+ aria-label={fieldAriaLabel}
1812
+ className={`${className} template-selector`}
1813
+ />
1814
+ );
1815
+ };
1816
+ const expressionBody = () => (
1817
+ <ReqoreControlGroup vertical fluid gapSize='small'>
1818
+ <ReqoreErrorBoundary>
1819
+ <ExpressionField
1820
+ /* A value that is not an expression yet is shown as the server reads it written alone:
1821
+ value(12) - so the Visual tab starts from the value, not from an operand with no operation. */
1822
+ value={{
1823
+ is_expression: true,
1824
+ value:
1825
+ value !== null && typeof value === 'object' ? value
1826
+ : !isEmptyValue && value !== null ?
1827
+ { exp: 'value', args: [{ type: type as TQorusType, value }] }
1828
+ : undefined,
1829
+ }}
1830
+ initialText={expressionSeed ?? undefined}
1831
+ heldAsValue={!isEmptyValue && value !== null && typeof value !== 'object'}
1832
+ componentOverrides={componentOverrides}
1833
+ localTemplates={templates}
1834
+ type={type as string}
1835
+ returnType={(returnType || expressionDataType) as any}
1836
+ onChange={handleExpressionChange}
1837
+ readOnly={rest.readOnly || rest.disabled}
1838
+ expressions={rest.expressions}
1839
+ expressionsUrl={rest.expressions_url}
1840
+ fields={rest.dpql_fields}
1841
+ serverHandled={rest.server_expression_handling}
1842
+ extraActions={extraActions}
1843
+ size={rest.size}
1844
+ defaultMode={tab === 'visual' ? 'visual' : 'text'}
1845
+ requestedMode={tab === 'visual' ? 'visual' : 'text'}
1846
+ hideModeToggle
1847
+ // no language server: the Visual tab is where the expression is built
1848
+ onTextUnavailable={() => setTab('visual')}
1849
+ reorder={reorder}
1850
+ />
1851
+ </ReqoreErrorBoundary>
1852
+ {expressionFromText !== null ?
1853
+ <ReqoreButton
1854
+ fixed
1855
+ compact
1856
+ minimal
1857
+ icon='ArrowGoBackLine'
1858
+ size={rest.size}
1859
+ className='dpql-detected-undo'
1860
+ tooltip={`Keep "${expressionFromText}" as a value instead`}
1861
+ onClick={undoExpressionFromText}
1862
+ >
1863
+ Undo
1864
+ </ReqoreButton>
1865
+ : null}
1866
+ </ReqoreControlGroup>
1867
+ );
1868
+ const templateBody = () => (
1869
+ <TemplateDropdownSelector
1870
+ allowCustomValues={false}
1871
+ templates={templates}
1872
+ value={typeof value === 'string' && isValueTemplate(value) ? value : null}
1873
+ items={filteredTemplates?.items}
1874
+ onItemSelect={handleSelectTemplateFromList}
1875
+ onRemoveClick={replaceWithAValue}
1876
+ size={rest.size}
1877
+ label={label}
1878
+ hasOnlyAllowedValues={hasOnlyAllowedValues}
1879
+ />
1880
+ );
1881
+ return (
1882
+ <ReqoreControlGroup vertical fluid gapSize='small' className='value-tabs-field'>
1883
+ <ReqoreControlGroup fluid size={rest.size} verticalAlign='center'>
1884
+ <ReqoreControlGroup stack size={rest.size} className='value-tabs' fixed>
1885
+ {tabButton('value', 'Value', 'EditLine')}
1886
+ {tabsKind === 'full' ?
1887
+ [
1888
+ tabButton(
1889
+ 'expression',
1890
+ 'Expression',
1891
+ 'CodeLine',
1892
+ 'Write an expression - it also takes templates'
1893
+ ),
1894
+ tabButton('visual', 'Visual', 'NodeTree', 'Build the expression visually'),
1895
+ ]
1896
+ : tabButton('template', 'Template', 'MoneyDollarCircleLine', 'Choose a template')}
1897
+ </ReqoreControlGroup>
1898
+ {renderControls()}
1899
+ </ReqoreControlGroup>
1900
+ {tab === 'value' ?
1901
+ valueBody()
1902
+ : tab === 'template' ?
1903
+ templateBody()
1904
+ : expressionBody()}
1905
+ </ReqoreControlGroup>
1906
+ );
1907
+ }
1908
+
1506
1909
  if (effectiveIsFunction && !hasOnlyAllowedValues) {
1507
1910
  // SEAM (reqraft): `allowTextExpressions` swaps the IDE's bare builder
1508
1911
  // for the ExpressionField shell (Visual = the same builder, Text = the
@@ -1634,21 +2037,65 @@ const TemplateFieldImpl = memo(
1634
2037
  />
1635
2038
  : null}
1636
2039
 
2040
+ {/* A date keeps its date control - it is not written as text - and takes a template beside it:
2041
+ chosen, the template is the value (the template control, whose × returns to the date). */}
2042
+ {/* A date keeps its date control - it is not written as text - and takes a template beside it:
2043
+ chosen, the template is the value (the template control, whose × returns to the date). */}
2044
+ {!isTemplate && allowCustomValues && dateTakesTemplates ?
2045
+ <ReqoreDropdown
2046
+ className='date-template-picker'
2047
+ icon='MoneyDollarCircleLine'
2048
+ fixed
2049
+ compact
2050
+ size={rest.size}
2051
+ tooltip='Use a template'
2052
+ aria-label='Use a template'
2053
+ filterable
2054
+ items={filteredTemplates?.items}
2055
+ onItemSelect={handleSelectDateTemplate}
2056
+ />
2057
+ : null}
2058
+
1637
2059
  {/* Template mode's editor for a value that can also be typed: each
1638
2060
  reference in it is a chip named as the catalogue names it, while the
1639
2061
  field still stores the plain string. A textarea spelled a chosen
1640
2062
  template `$local:name` wherever it was edited — in the Visual
1641
2063
  builder's operands, in every string field that takes templates. */}
1642
- {isTemplate && templateSupportsCustomValues && !templateValueIsBracedToken ?
2064
+ {isTemplate && templateSupportsCustomValues ?
1643
2065
  <RichTextFormField
1644
2066
  className='template-selector'
1645
2067
  valueFormat='text'
1646
2068
  singleLine={isSingleLineStringType('string')}
1647
- value={typeof templateValue === 'string' ? templateValue : ''}
2069
+ /* null shows as null, not as an empty field: the two are different values, and an
2070
+ untyped field holding null looked exactly like one holding nothing (qorus#646) */
2071
+ value={
2072
+ typeof templateValue === 'string' ? templateValue : (
2073
+ (untypedTextOf(value, type as string) ?? '')
2074
+ )
2075
+ }
1648
2076
  templates={filteredTemplates}
2077
+ // a chip of a field not of this type (text around it, say) is named as the catalogue names it
2078
+ namingTemplates={templates}
1649
2079
  allowTemplates
1650
2080
  onChange={handleTemplateTextChange}
1651
2081
  {...rest}
2082
+ /* The text the value is written in takes the room its row gives it and gives it back: a
2083
+ caller's \`fixed\` sized a picker chip, and on a text field it kept the field at its full
2084
+ width, pushing the ⋮ past an operand's row in a narrow column. */
2085
+ fixed={false}
2086
+ fluid
2087
+ /* 150px wide, as a free text field is, wider for what it holds, and narrower only where its
2088
+ row has less room - an operand in a narrow column - as a number's input is: a fixed
2089
+ 150px minimum held it at full width there. */
2090
+ panelProps={{
2091
+ ...(rest as { panelProps?: object }).panelProps,
2092
+ style: {
2093
+ ...(rest as { panelProps?: { style?: object } }).panelProps?.style,
2094
+ width: '150px',
2095
+ maxWidth: '100%',
2096
+ minWidth: 'fit-content',
2097
+ },
2098
+ }}
1652
2099
  aria-label={fieldAriaLabel}
1653
2100
  />
1654
2101
  : null}
@@ -1676,10 +2123,7 @@ const TemplateFieldImpl = memo(
1676
2123
  </ReqoreControlGroup>
1677
2124
  : null}
1678
2125
 
1679
- {(
1680
- showTemplatesDropdown ||
1681
- (isTemplate && templateSupportsCustomValues && templateValueIsBracedToken)
1682
- ) ?
2126
+ {showTemplatesDropdown ?
1683
2127
  <TemplateDropdownSelector
1684
2128
  allowCustomValues={allowCustomValues}
1685
2129
  templates={templates}