@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g124fc92

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 (161) hide show
  1. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  2. package/dist/components/form/engine/CompactRow.js +130 -25
  3. package/dist/components/form/engine/CompactRow.js.map +1 -1
  4. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  5. package/dist/components/form/engine/CompactToolbar.js +78 -15
  6. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  7. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  8. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  9. package/dist/components/form/engine/FormEngine.js +260 -99
  10. package/dist/components/form/engine/FormEngine.js.map +1 -1
  11. package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
  12. package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
  13. package/dist/components/form/engine/OptionFieldMessages.js +22 -3
  14. package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
  15. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  16. package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
  17. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  18. package/dist/components/form/engine/compactRowContext.d.ts +5 -0
  19. package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
  20. package/dist/components/form/engine/compactRowContext.js.map +1 -1
  21. package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
  22. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  23. package/dist/components/form/engine/compactRowStyles.js +72 -39
  24. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  25. package/dist/components/form/engine/compactToolbarContext.d.ts +3 -0
  26. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  28. package/dist/components/form/engine/formAppearance.d.ts +48 -0
  29. package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
  30. package/dist/components/form/engine/formAppearance.js +50 -0
  31. package/dist/components/form/engine/formAppearance.js.map +1 -0
  32. package/dist/components/form/engine/readFirst.d.ts +14 -2
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +6 -2
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/engine/rowFocus.d.ts +9 -1
  37. package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
  38. package/dist/components/form/engine/rowFocus.js +17 -5
  39. package/dist/components/form/engine/rowFocus.js.map +1 -1
  40. package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
  41. package/dist/components/form/expressions/renderExpressionToText.js +21 -0
  42. package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
  43. package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
  44. package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
  45. package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
  46. package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
  47. package/dist/components/form/fields/Field.d.ts.map +1 -1
  48. package/dist/components/form/fields/Field.js +13 -11
  49. package/dist/components/form/fields/Field.js.map +1 -1
  50. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  51. package/dist/components/form/fields/allowed-values/AllowedValues.js +20 -7
  52. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  53. package/dist/components/form/fields/allowed-values/savedValues.d.ts +30 -0
  54. package/dist/components/form/fields/allowed-values/savedValues.d.ts.map +1 -0
  55. package/dist/components/form/fields/allowed-values/savedValues.js +18 -0
  56. package/dist/components/form/fields/allowed-values/savedValues.js.map +1 -0
  57. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  58. package/dist/components/form/fields/auto/AutoFormField.js +18 -8
  59. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  60. package/dist/components/form/fields/long-string/LongString.d.ts +7 -1
  61. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  62. package/dist/components/form/fields/long-string/LongString.js +9 -5
  63. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  64. package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
  65. package/dist/components/form/fields/multi-select/MultiSelectFormField.js +6 -5
  66. package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
  67. package/dist/components/form/fields/number/Number.d.ts +4 -1
  68. package/dist/components/form/fields/number/Number.d.ts.map +1 -1
  69. package/dist/components/form/fields/number/Number.js +13 -5
  70. package/dist/components/form/fields/number/Number.js.map +1 -1
  71. package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
  72. package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
  73. package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
  74. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  75. package/dist/components/form/fields/template/TemplateField.js +71 -42
  76. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  77. package/dist/components/form/index.d.ts +2 -0
  78. package/dist/components/form/index.d.ts.map +1 -1
  79. package/dist/components/form/index.js +2 -0
  80. package/dist/components/form/index.js.map +1 -1
  81. package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -1
  82. package/dist/components/imageLightbox/ImageLightbox.js +2 -0
  83. package/dist/components/imageLightbox/ImageLightbox.js.map +1 -1
  84. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +3 -2
  85. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
  86. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
  87. package/dist/components/supportTicket/ReferencePicker.d.ts +2 -1
  88. package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
  89. package/dist/components/supportTicket/ReferencePicker.js +19 -13
  90. package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
  91. package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
  92. package/dist/components/supportTicket/TicketHeader.js +4 -1
  93. package/dist/components/supportTicket/TicketHeader.js.map +1 -1
  94. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
  95. package/dist/components/supportTicket/TicketReplyBox.js +29 -10
  96. package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
  97. package/dist/components/supportTicket/useLoadedImages.d.ts +10 -0
  98. package/dist/components/supportTicket/useLoadedImages.d.ts.map +1 -1
  99. package/dist/components/supportTicket/useLoadedImages.js +63 -1
  100. package/dist/components/supportTicket/useLoadedImages.js.map +1 -1
  101. package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
  102. package/dist/components/ticketReferences/TicketReferences.js +6 -2
  103. package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
  104. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  105. package/dist/components/ticketThread/TicketThread.js +10 -16
  106. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  107. package/dist/helpers/allowedValues.d.ts +12 -0
  108. package/dist/helpers/allowedValues.d.ts.map +1 -1
  109. package/dist/helpers/allowedValues.js +20 -1
  110. package/dist/helpers/allowedValues.js.map +1 -1
  111. package/dist/helpers/fieldDescription.d.ts +16 -0
  112. package/dist/helpers/fieldDescription.d.ts.map +1 -0
  113. package/dist/helpers/fieldDescription.js +40 -0
  114. package/dist/helpers/fieldDescription.js.map +1 -0
  115. package/dist/helpers/singleLineString.d.ts.map +1 -1
  116. package/dist/helpers/singleLineString.js +1 -0
  117. package/dist/helpers/singleLineString.js.map +1 -1
  118. package/package.json +1 -1
  119. package/src/components/form/engine/CompactRow.tsx +143 -23
  120. package/src/components/form/engine/CompactToolbar.tsx +85 -16
  121. package/src/components/form/engine/FormEngine.stories.tsx +25 -13
  122. package/src/components/form/engine/FormEngine.tsx +367 -81
  123. package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
  124. package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
  125. package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
  126. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
  127. package/src/components/form/engine/compactRowContext.ts +5 -0
  128. package/src/components/form/engine/compactRowStyles.ts +218 -38
  129. package/src/components/form/engine/compactToolbarContext.ts +3 -0
  130. package/src/components/form/engine/formAppearance.ts +78 -0
  131. package/src/components/form/engine/readFirst.ts +16 -3
  132. package/src/components/form/engine/rowFocus.ts +17 -6
  133. package/src/components/form/expressions/renderExpressionToText.ts +15 -0
  134. package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
  135. package/src/components/form/fields/Field.tsx +11 -9
  136. package/src/components/form/fields/allowed-values/AllowedValues.tsx +21 -9
  137. package/src/components/form/fields/allowed-values/savedValues.ts +41 -0
  138. package/src/components/form/fields/auto/AutoFormField.tsx +17 -4
  139. package/src/components/form/fields/long-string/LongString.stories.tsx +27 -0
  140. package/src/components/form/fields/long-string/LongString.tsx +14 -3
  141. package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +5 -4
  142. package/src/components/form/fields/number/Number.tsx +19 -4
  143. package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
  144. package/src/components/form/fields/template/TemplateField.tsx +32 -4
  145. package/src/components/form/index.tsx +2 -0
  146. package/src/components/imageLightbox/ImageLightbox.tsx +2 -0
  147. package/src/components/supportTicket/InterfaceReferenceTags.tsx +3 -2
  148. package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -4
  149. package/src/components/supportTicket/ReferencePicker.tsx +22 -12
  150. package/src/components/supportTicket/TicketConversation.stories.tsx +0 -1
  151. package/src/components/supportTicket/TicketHeader.tsx +2 -1
  152. package/src/components/supportTicket/TicketReplyBox.stories.tsx +24 -21
  153. package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
  154. package/src/components/supportTicket/useLoadedImages.ts +20 -0
  155. package/src/components/ticketReferences/TicketReferences.stories.tsx +26 -0
  156. package/src/components/ticketReferences/TicketReferences.tsx +12 -4
  157. package/src/components/ticketThread/TicketThread.stories.tsx +71 -2
  158. package/src/components/ticketThread/TicketThread.tsx +15 -28
  159. package/src/helpers/allowedValues.ts +19 -0
  160. package/src/helpers/fieldDescription.ts +39 -0
  161. package/src/helpers/singleLineString.ts +1 -0
@@ -2,7 +2,6 @@ import {
2
2
  IReqoreLabelProps,
3
3
  ReqoreCheckbox,
4
4
  ReqoreControlGroup,
5
- ReqoreSpan,
6
5
  } from '@qoretechnologies/reqore';
7
6
  import { IWithReqoreSize } from '@qoretechnologies/reqore/dist/types/global';
8
7
  import { TReqoreBadge } from '@qoretechnologies/reqore/dist/components/Button';
@@ -34,6 +33,7 @@ import {
34
33
  UNAVAILABLE_ITEM_CLASS,
35
34
  } from './select/SelectCollection';
36
35
  import { useAllowedValueAvailability } from '../engine/OptionFieldMessages';
36
+ import { ChoiceGroupHeading, useSelectedChoiceIntent } from './ChoiceGroupHeading';
37
37
  import { StringFormField } from './string/String';
38
38
  import { ArrayAutoField } from './array/ArrayAutoField';
39
39
  import { AutoFormField } from './auto/AutoFormField';
@@ -46,6 +46,7 @@ import { IDataSchemaDefinition } from './schema-definition/types';
46
46
  // because modules are all loaded before any component renders
47
47
  import { FormEngine } from '../engine/FormEngine';
48
48
  import { FormFieldsSkeleton } from '../engine/FormFieldsSkeleton';
49
+ import { allowedValueOf } from '../../../helpers/allowedValues';
49
50
 
50
51
  /** Soft Qore types (softint, softstring, …) render as their concrete base —
51
52
  * the soft/hard distinction is value-level, not UI. */
@@ -152,6 +153,7 @@ export const FormField = <T extends TFormFieldType>({
152
153
  ...rest
153
154
  }: IFormFieldProps<T>) => {
154
155
  const id = useId();
156
+ const selectedChoiceIntent = useSelectedChoiceIntent();
155
157
  const { schema: finalArgSchema, loading: argSchemaLoading } = useArgSchema(arg_schema);
156
158
 
157
159
  const handleChange = (value: TFormFieldValueType<T>, event?: unknown) => {
@@ -415,7 +417,7 @@ export const FormField = <T extends TFormFieldType>({
415
417
  if (size(element_allowed_values) && !element_allowed_values_creatable) {
416
418
  const formatToServer = (vals: unknown[]) =>
417
419
  (vals || []).map((v) => {
418
- const match = element_allowed_values.find((ev) => isEqual(ev.value?.value, v));
420
+ const match = element_allowed_values.find((ev) => isEqual(allowedValueOf(ev), v));
419
421
  return { value: v, type: match?.value?.type ?? effectiveElementType };
420
422
  });
421
423
  const formatFromServer = (vals: unknown[]) =>
@@ -426,7 +428,7 @@ export const FormField = <T extends TFormFieldType>({
426
428
  return (
427
429
  <ReqoreControlGroup vertical size='tiny'>
428
430
  {element_allowed_values.map((item, index) => {
429
- const itemValue = item.value?.value;
431
+ const itemValue = allowedValueOf(item);
430
432
  const checked = serverValue.some((v) => isEqual(v, itemValue));
431
433
  return (
432
434
  <ReqoreCheckbox
@@ -435,7 +437,7 @@ export const FormField = <T extends TFormFieldType>({
435
437
  label={item.display_name ?? String(itemValue)}
436
438
  tooltip={item.short_desc || item.desc}
437
439
  checked={checked}
438
- intent={checked ? 'info' : undefined}
440
+ intent={selectedChoiceIntent(checked)}
439
441
  onClick={() => {
440
442
  const next =
441
443
  checked ?
@@ -590,7 +592,7 @@ export const FormField = <T extends TFormFieldType>({
590
592
  resolved && !resolved.available ?
591
593
  { title: resolved.title, content: resolved.reason as string, intent: resolved.intent }
592
594
  : undefined,
593
- value: item.value?.value,
595
+ value: allowedValueOf(item),
594
596
  };
595
597
  };
596
598
 
@@ -653,13 +655,13 @@ export const FormField = <T extends TFormFieldType>({
653
655
  if (allowed_values.length <= 3) {
654
656
  return (
655
657
  <ReqoreControlGroup vertical size='tiny'>
656
- <ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
658
+ <ChoiceGroupHeading>
657
659
  {/* Headed by the field's own name; "Select one:" only when it has
658
660
  none. Same rule as FieldAllowedValuesCheckGroup. */}
659
661
  {display_name ? `${display_name}:` : 'Select one:'}
660
- </ReqoreSpan>
662
+ </ChoiceGroupHeading>
661
663
  {allowed_values.map((item, index) => {
662
- const itemValue = item.value?.value;
664
+ const itemValue = allowedValueOf(item);
663
665
  const checked = isEqual(value, itemValue);
664
666
  const mapped = mapAllowedValue(item, index);
665
667
  const unavailable = getSelectItemUnavailability(mapped);
@@ -695,7 +697,7 @@ export const FormField = <T extends TFormFieldType>({
695
697
  readOnly={!!unavailable}
696
698
  uncheckedIcon={unavailable ? 'LockLine' : undefined}
697
699
  checked={checked}
698
- intent={checked ? 'info' : undefined}
700
+ intent={selectedChoiceIntent(checked)}
699
701
  onClick={
700
702
  unavailable ? undefined : (
701
703
  () => handleChange((checked ? undefined : itemValue) as TFormFieldValueType<T>)
@@ -2,13 +2,13 @@
2
2
  // (FIELD_STACK_REPORT batch). Markup, props and the debounced local-value
3
3
  // flow are preserved. Seams (IDE-only infrastructure, same classes as the
4
4
  // ExpressionBuilder port):
5
- // - `useSavedValues` — saved-values storage is IDE-only; `showSavedValues`
6
- // stays in the interface but contributes no items.
5
+ // - `useSavedValues` — saved-values storage is the host's; a field that shows
6
+ // saved values reads them from `SavedValuesContext` (see `savedValues.ts`).
7
7
  // - `useSubscriptionEvents` (CONNECTION_DELETED pruning) — dropped.
8
8
  // - `ConnectionManagement` per-item actions — dropped (actions omitted).
9
9
  // - `Select` → reqraft `SelectFormField` (`items` instead of `defaultItems`,
10
10
  // single-arg `onChange`; `type`/`closeCollectionOnUnMount` not forwarded).
11
- import { ReqoreCheckbox, ReqoreControlGroup, ReqoreSpan } from '@qoretechnologies/reqore';
11
+ import { ReqoreCheckbox, ReqoreControlGroup } from '@qoretechnologies/reqore';
12
12
  import { IReqoreButtonProps } from '@qoretechnologies/reqore/dist/components/Button';
13
13
  import { IQorusAllowedValue, TQorusType } from '@qoretechnologies/ts-toolkit';
14
14
  import { size as count, isEqual } from 'lodash';
@@ -20,7 +20,10 @@ import {
20
20
  } from '../select/SelectCollection';
21
21
  import { ISelectFormFieldItem, SelectFormField as Select } from '../select/Select';
22
22
  import { useAllowedValueAvailability } from '../../engine/OptionFieldMessages';
23
+ import { ChoiceGroupHeading, useSelectedChoiceIntent } from '../ChoiceGroupHeading';
23
24
  import type { IReqraftAllowedValue } from '../Field';
25
+ import { allowedValueOf } from '../../../../helpers/allowedValues';
26
+ import { useSavedValueChoices } from './savedValues';
24
27
 
25
28
  export interface IFieldAllowedValuesProps extends Pick<
26
29
  IReqoreButtonProps,
@@ -97,13 +100,15 @@ export const FieldAllowedValuesCheckGroup = memo(
97
100
  */
98
101
  label?: string;
99
102
  }) => {
103
+ const selectedChoiceIntent = useSelectedChoiceIntent();
104
+
100
105
  return (
101
106
  <ReqoreControlGroup vertical size={rest.size || 'small'} gapSize='tiny'>
102
- <ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
107
+ <ChoiceGroupHeading>
103
108
  {label ? `${label}:`
104
109
  : multiSelect ? 'Select one or more:'
105
110
  : 'Select one:'}
106
- </ReqoreSpan>
111
+ </ChoiceGroupHeading>
107
112
  {items?.map((item) => {
108
113
  const unavailable = getSelectItemUnavailability(item);
109
114
 
@@ -134,7 +139,7 @@ export const FieldAllowedValuesCheckGroup = memo(
134
139
  which keeps the pointer events the tooltip is read through.
135
140
  It does not withhold `onClick` — the handler below does. */
136
141
  readOnly={rest.readOnly || !!unavailable}
137
- intent={item.value === value ? 'info' : undefined}
142
+ intent={selectedChoiceIntent(item.value === value)}
138
143
  // `isEqual`, and only once there is a value to match. The single
139
144
  // -select branch compared JSON.stringify(value) with
140
145
  // JSON.stringify(item.value), and JSON.stringify(undefined) is
@@ -194,6 +199,8 @@ export const FieldAllowedValues = memo(
194
199
  `depends_on`, and a refusal the server had already decided. Both end up
195
200
  as one state on the item: no click handler, and a reason to read. */
196
201
  const availability = useAllowedValueAvailability(items as IReqraftAllowedValue[]);
202
+ const savedValues = useSavedValueChoices(type);
203
+ const offersSavedValues = !!showSavedValues && !readOnly && !disabled && count(savedValues) > 0;
197
204
 
198
205
  const fullItems = useMemo(() => {
199
206
  const result = [
@@ -202,7 +209,7 @@ export const FieldAllowedValues = memo(
202
209
  const resolved = availability[index];
203
210
 
204
211
  return {
205
- value: value?.value,
212
+ value: allowedValueOf({ value, name: rest.name }),
206
213
  ...rest,
207
214
  disabled: rest.disabled || resolved?.available === false,
208
215
  unavailable:
@@ -217,12 +224,17 @@ export const FieldAllowedValues = memo(
217
224
  }),
218
225
  ] as ISelectFormFieldItem[];
219
226
 
227
+ if (offersSavedValues) {
228
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
229
+ result.push(...savedValues.map(({ valueType, ...saved }) => saved));
230
+ }
231
+
220
232
  return result;
221
- }, [JSON.stringify(items), availability, type, value, showSavedValues, disabled, readOnly]);
233
+ }, [JSON.stringify(items), availability, type, value, offersSavedValues, savedValues]);
222
234
 
223
235
  const style = useMemo(() => ({ width: '100%' }), []);
224
236
 
225
- if (!count(fullItems) || type === 'enum' || (!count(items) && !showSavedValues)) {
237
+ if (!count(fullItems) || type === 'enum' || (!count(items) && !offersSavedValues)) {
226
238
  return null;
227
239
  }
228
240
 
@@ -0,0 +1,41 @@
1
+ import { createContext, useContext, useMemo } from 'react';
2
+ import { ISelectFormFieldItem } from '../select/Select';
3
+
4
+ /**
5
+ * SEAM (reqraft): values a person saved to reuse in other fields.
6
+ *
7
+ * Where they are kept is the host's business — the IDE stores them in the
8
+ * user's settings — so this package draws them and asks the host to save one;
9
+ * it never stores anything itself. Without a provider a form offers neither,
10
+ * which is what it did before the seam existed (`showSavedValues` and
11
+ * `allowSaving` were inert).
12
+ */
13
+ export interface ISavedValueChoice extends ISelectFormFieldItem {
14
+ /** The type of the saved value: a field offers only the values of its own type. */
15
+ valueType?: string;
16
+ }
17
+
18
+ export interface ISavedValues {
19
+ /** Every saved value, drawn as choices by a field that shows saved values. */
20
+ items: ISavedValueChoice[];
21
+ /** Saves a field's current value; the host asks what to call it. */
22
+ saveValue?: (field: { value: unknown; type?: string }) => void;
23
+ /** The label of the row-menu item that saves a value. */
24
+ saveLabel?: string;
25
+ }
26
+
27
+ export const SavedValuesContext = createContext<ISavedValues | undefined>(undefined);
28
+
29
+ const NO_SAVED_VALUES: ISavedValueChoice[] = [];
30
+
31
+ /** The saved values a field of `type` can choose from. */
32
+ export const useSavedValueChoices = (type?: string): ISavedValueChoice[] => {
33
+ const items = useContext(SavedValuesContext)?.items;
34
+ return useMemo(
35
+ () =>
36
+ !items ? NO_SAVED_VALUES
37
+ : type ? items.filter((item) => !item.valueType || item.valueType === type)
38
+ : items,
39
+ [items, type]
40
+ );
41
+ };
@@ -17,6 +17,7 @@ import React, { memo, useCallback, useEffect, useState } from 'react';
17
17
  import { useUpdateEffect } from 'react-use';
18
18
  import useMount from 'react-use/lib/useMount';
19
19
  import { typedToYaml, yamlToTyped } from '../../../../helpers/common';
20
+ import { allowedValueOf } from '../../../../helpers/allowedValues';
20
21
  import { getListElementValue } from '../../../../helpers/options';
21
22
  import { isSingleLineStringType } from '../../../../helpers/singleLineString';
22
23
  import {
@@ -658,9 +659,21 @@ function AutoField<T = any>({
658
659
  Without the type the field cannot tell them apart and treats them all
659
660
  as documents, so an interface's Internal Name — which becomes a YAML
660
661
  key — accepted Enter. Only text takes references; `data` and
661
- `binary` are encoded content, where a chip would stand for nothing. */
662
+ `binary` are encoded content, where a chip would stand for nothing.
663
+
664
+ The semantic string formats (`email` … `phone`) are one line of text
665
+ whose identity the validator checks. They are in the option vocabulary
666
+ (`optionUiTypes`) and the single-line list, but had no case here, so a
667
+ field of one drew "Unknown type!" in every form. */
662
668
  case 'string':
663
669
  case 'long-string':
670
+ case 'email':
671
+ case 'uri':
672
+ case 'uuid':
673
+ case 'hostname':
674
+ case 'ipv4':
675
+ case 'ipv6':
676
+ case 'phone':
664
677
  return renderTextEditor(currentType);
665
678
  case 'data':
666
679
  case 'binary':
@@ -974,8 +987,8 @@ function AutoField<T = any>({
974
987
  // previously only `allowed_values` was read, leaving `items` empty.
975
988
  const enumItems = (rest.allowed_values as any[] | undefined)?.length
976
989
  ? (rest.allowed_values as any[]).map((av) => ({
977
- label: av.display_name ?? String(av.value?.value ?? av.value),
978
- value: av.value?.value ?? av.value,
990
+ label: av.display_name ?? String(allowedValueOf(av)),
991
+ value: allowedValueOf(av),
979
992
  image: av.image,
980
993
  icon: av.icon,
981
994
  }))
@@ -1008,7 +1021,7 @@ function AutoField<T = any>({
1008
1021
  <SelectFormField
1009
1022
  items={(rest.allowed_values || []).map((av) => ({
1010
1023
  ...av,
1011
- value: av.value?.value,
1024
+ value: allowedValueOf(av),
1012
1025
  }))}
1013
1026
  value={value}
1014
1027
  onChange={(val) => handleChange(name, val)}
@@ -118,3 +118,30 @@ export const Disabled: Story = {
118
118
  await expect(textarea).toHaveValue('Cannot edit this');
119
119
  },
120
120
  };
121
+
122
+ /**
123
+ * `debounceMs={0}` passes every change on at once. A form submitted by Enter (a Qonsole
124
+ * question) reads the value the moment the key goes down; with the usual wait it would
125
+ * submit the text as it was before the last few keys.
126
+ */
127
+ export const PassedOnAtOnce: Story = {
128
+ args: {
129
+ debounceMs: 0,
130
+ },
131
+ parameters: {
132
+ docs: {
133
+ description: {
134
+ story:
135
+ 'Renders the LongString field with no wait before a change is passed on, so each key reaches the form immediately — what a form submitted by Enter needs.',
136
+ },
137
+ },
138
+ },
139
+ async play({ canvasElement, args }) {
140
+ const canvas = within(canvasElement);
141
+ const textarea = canvas.getByLabelText('LongString');
142
+
143
+ await userEvent.type(textarea, 'abc');
144
+ // no waitFor: the last key is already with the form
145
+ await expect(args.onChange).toHaveBeenLastCalledWith('abc');
146
+ },
147
+ };
@@ -20,6 +20,12 @@ export interface ILongStringFormFieldProps extends Omit<IReqoreTextareaProps, 'o
20
20
  * names and URLs with a newline in them. See `helpers/singleLineString`.
21
21
  */
22
22
  type?: string;
23
+ /**
24
+ * How long the text waits after the last key before it is passed on (ms). `0` passes
25
+ * every change on at once: a form submitted by Enter — a Qonsole question — must see
26
+ * the last thing typed, not the text from 100 ms earlier.
27
+ */
28
+ debounceMs?: number;
23
29
  }
24
30
 
25
31
  export const LongStringFormField = ({
@@ -27,6 +33,7 @@ export const LongStringFormField = ({
27
33
  onChange,
28
34
  onClearClick,
29
35
  type,
36
+ debounceMs = 100,
30
37
  ...rest
31
38
  }: ILongStringFormFieldProps) => {
32
39
  const [localValue, setLocalValue] = useState<string>(value ?? '');
@@ -72,7 +79,7 @@ export const LongStringFormField = ({
72
79
  emit(localValue);
73
80
  }
74
81
  },
75
- 100,
82
+ debounceMs,
76
83
  [localValue, emit]
77
84
  );
78
85
 
@@ -84,9 +91,13 @@ export const LongStringFormField = ({
84
91
  // Flatten here rather than only blocking the Enter key: a line break also
85
92
  // arrives by paste, by drag-and-drop, from autofill and from an IME, and
86
93
  // a keydown guard catches none of those.
87
- setLocalValue(singleLine && hasLineBreak(raw) ? flattenToSingleLine(raw) : raw);
94
+ const next = singleLine && hasLineBreak(raw) ? flattenToSingleLine(raw) : raw;
95
+ setLocalValue(next);
96
+ if (debounceMs === 0 && next !== lastEmittedRef.current) {
97
+ emit(next);
98
+ }
88
99
  },
89
- [singleLine]
100
+ [singleLine, debounceMs, emit]
90
101
  );
91
102
 
92
103
  // Stop Enter before it inserts anything, so the caret does not jump to a
@@ -7,6 +7,7 @@ import {
7
7
  getSelectItemUnavailability,
8
8
  UNAVAILABLE_ITEM_CLASS,
9
9
  } from '../select/SelectCollection';
10
+ import { allowedValueOf } from '../../../../helpers/allowedValues';
10
11
 
11
12
  export interface IMultiSelectFormFieldProps {
12
13
  /** Currently selected values (plain scalars) */
@@ -55,7 +56,7 @@ export const MultiSelectFormField = memo(
55
56
  messages: item.messages,
56
57
  })
57
58
  )
58
- .map((item) => String(item.value?.value ?? ''))
59
+ .map((item) => String(allowedValueOf(item) ?? ''))
59
60
  ),
60
61
  [JSON.stringify(items)]
61
62
  );
@@ -75,8 +76,8 @@ export const MultiSelectFormField = memo(
75
76
  });
76
77
 
77
78
  return {
78
- value: item.value?.value as string,
79
- label: item.display_name ?? String(item.value?.value ?? ''),
79
+ value: allowedValueOf(item) as string,
80
+ label: item.display_name ?? String(allowedValueOf(item) ?? ''),
80
81
  description:
81
82
  unavailable ?
82
83
  [unavailable.title, unavailable.content, description].filter(Boolean).join(' — ')
@@ -91,7 +92,7 @@ export const MultiSelectFormField = memo(
91
92
  That one stays reqore's `disabled` — the wildcard is a state of
92
93
  the SELECTION rather than a refusal of the value, and it has no
93
94
  reason of its own to protect. */
94
- disabled: selectedValues.includes('*') && item.value?.value !== '*',
95
+ disabled: selectedValues.includes('*') && allowedValueOf(item) !== '*',
95
96
  };
96
97
  }
97
98
  );
@@ -25,6 +25,9 @@ export interface INumberFormFieldProps extends Omit<IReqoreInputProps, 'value' |
25
25
  */
26
26
  min_value?: number;
27
27
  max_value?: number;
28
+ /** How long a typed number waits after the last key before it is passed on (ms); `0`
29
+ * passes every change on at once, for a form submitted by Enter. */
30
+ debounceMs?: number;
28
31
  }
29
32
 
30
33
  /** A whole number as typed: an optional sign and digits. */
@@ -51,6 +54,7 @@ export const NumberFormField = ({
51
54
  templates,
52
55
  min_value,
53
56
  max_value,
57
+ debounceMs = 100,
54
58
  ...rest
55
59
  }: INumberFormFieldProps) => {
56
60
  // What is shown is what was typed; what the form gets is the number it writes, or the text when it writes
@@ -77,13 +81,24 @@ export const NumberFormField = ({
77
81
  onChange?.(localValue);
78
82
  }
79
83
  },
80
- 100,
84
+ debounceMs,
81
85
  [localValue, onChange]
82
86
  );
83
87
 
84
- const handleChange = useCallback((rawValue: number | string): void => {
85
- setText(String(rawValue));
86
- }, []);
88
+ const handleChange = useCallback(
89
+ (rawValue: number | string): void => {
90
+ const nextText = String(rawValue);
91
+ setText(nextText);
92
+ if (debounceMs === 0) {
93
+ const next = nextText === '' ? '' : typedNumber(nextText, type);
94
+ if (next !== value) {
95
+ emitted.record(next);
96
+ onChange?.(next);
97
+ }
98
+ }
99
+ },
100
+ [debounceMs, type, value, onChange]
101
+ );
87
102
 
88
103
  const handleInputChange = useCallback(
89
104
  (event: ChangeEvent<HTMLInputElement>): void => {
@@ -1,6 +1,7 @@
1
1
  import { ReqoreRadioGroup } from '@qoretechnologies/reqore';
2
2
  import { IReqoreRadioGroupProps } from '@qoretechnologies/reqore/dist/components/RadioGroup';
3
3
  import { useMemo } from 'react';
4
+ import { useIsCalmForm } from '../../engine/formAppearance';
4
5
 
5
6
  export interface IRadioGroupFormFieldProps
6
7
  extends Omit<IReqoreRadioGroupProps, 'onChange' | 'onSelectClick' | 'selected'> {
@@ -16,20 +17,25 @@ export const RadioGroupFormField = ({
16
17
  value,
17
18
  ...rest
18
19
  }: IRadioGroupFormFieldProps) => {
20
+ const calm = useIsCalmForm();
19
21
  const items: IReqoreRadioGroupProps['items'] = useMemo(
20
22
  () =>
21
23
  _items.map((item) => ({
22
24
  margin: 'right',
23
- labelEffect: {
24
- spaced: 1,
25
- weight: 'bold',
26
- uppercase: true,
27
- textSize: 'small',
28
- },
25
+ // Calm: an option's name as written — a choice is read, not shouted.
26
+ labelEffect:
27
+ calm ?
28
+ { textSize: 'small' }
29
+ : {
30
+ spaced: 1,
31
+ weight: 'bold',
32
+ uppercase: true,
33
+ textSize: 'small',
34
+ },
29
35
  ...item,
30
36
  disabled: disabled || item.disabled,
31
37
  })),
32
- [_items]
38
+ [_items, calm]
33
39
  );
34
40
 
35
41
  return (
@@ -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, useRef, useState } from 'react';
32
+ import React, { memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
33
33
  import { useUpdateEffect } from 'react-use';
34
34
  import {
35
35
  filterTemplatesByType as templatesFilterFunc,
@@ -73,6 +73,7 @@ import {
73
73
  useRowMenu,
74
74
  useRowMenuPublisher,
75
75
  } from '../../engine/rowMenuContext';
76
+ import { SavedValuesContext } from '../allowed-values/savedValues';
76
77
 
77
78
  // Re-export template utilities for consumers
78
79
  export { getTemplateKey, getTemplateValue, isValueTemplate };
@@ -919,9 +920,22 @@ const TemplateFieldImpl = memo(
919
920
  [name, onChange]
920
921
  );
921
922
 
922
- // SEAM (reqraft): the IDE computes `canSaveValue` here and renders a
923
- // `SaveValueButton` in the controls menu — the saved-values storage is
924
- // IDE-only, so the menu item is dropped (`allowSaving` is inert).
923
+ /* Saving a value for reuse goes to the HOST, which keeps saved values and
924
+ asks what to call this one (`SavedValuesContext`). Offered where a value
925
+ can be typed in — a field of fixed choices has nothing of its own to
926
+ save, and neither does a yes/no — and only once there is a value. */
927
+ const savedValues = useContext(SavedValuesContext);
928
+ const canSaveValue =
929
+ !!rest.allowSaving &&
930
+ !!savedValues?.saveValue &&
931
+ (!rest.allowed_values || !!rest.allowed_values_creatable) &&
932
+ type !== 'bool' &&
933
+ type !== 'boolean' &&
934
+ !rest.readonly &&
935
+ !rest.disabled &&
936
+ value !== undefined &&
937
+ value !== null &&
938
+ value !== '';
925
939
 
926
940
  const handleRemoveTemplateClick = useCallback(() => {
927
941
  /* This is the `×` ON the template control, and it means "I do not want a
@@ -1218,6 +1232,15 @@ const TemplateFieldImpl = memo(
1218
1232
  },
1219
1233
  ]
1220
1234
  : []),
1235
+ ...(canSaveValue
1236
+ ? [
1237
+ {
1238
+ label: savedValues?.saveLabel ?? 'Save value',
1239
+ icon: 'SaveLine' as const,
1240
+ onClick: () => savedValues?.saveValue?.({ value, type }),
1241
+ },
1242
+ ]
1243
+ : []),
1221
1244
  /* The "set a value of this type" choices an untyped field offers.
1222
1245
  Dividers are dropped: they grouped items in a menu this field drew
1223
1246
  itself, and in the row's shared menu they would divide other
@@ -1243,6 +1266,10 @@ const TemplateFieldImpl = memo(
1243
1266
  handleSelectFunctionChange,
1244
1267
  handleTemplateToggleClick,
1245
1268
  handleUseCustomValueClick,
1269
+ canSaveValue,
1270
+ savedValues,
1271
+ value,
1272
+ type,
1246
1273
  ]
1247
1274
  );
1248
1275
 
@@ -1253,6 +1280,7 @@ const TemplateFieldImpl = memo(
1253
1280
  canOfferExpression && !functions.loading ? 'expression' : '',
1254
1281
  canOfferTemplate ? 'template' : '',
1255
1282
  canOfferCustomValue ? 'custom-value' : '',
1283
+ canSaveValue ? 'save-value' : '',
1256
1284
  `custom:${((menuItems ?? []) as { label?: unknown }[]).map((item) => String(item.label ?? '')).join('|')}`,
1257
1285
  ].join(',');
1258
1286
 
@@ -1,5 +1,6 @@
1
1
  export * from './fields/Field';
2
2
  export * from './fields/allowed-values/AllowedValues';
3
+ export * from './fields/allowed-values/savedValues';
3
4
  export * from './fields/array/ArrayAuto';
4
5
  export * from './fields/array/ArrayAutoField';
5
6
  export * from './fields/auto/AutoFormField';
@@ -24,6 +25,7 @@ export * from './fields/string/String';
24
25
  export * from './fields/template/ExampleValueModal';
25
26
  export * from './fields/template/TemplateField';
26
27
  export * from './engine/FormEngine';
28
+ export * from './engine/formAppearance';
27
29
  export * from './engine/FormFieldsSkeleton';
28
30
  export * from './engine/optionActions';
29
31
  export * from './engine/rendererTypes';
@@ -7,6 +7,7 @@ import {
7
7
  } from '@qoretechnologies/reqore';
8
8
  import { useCallback, useEffect, useRef, useState } from 'react';
9
9
  import styled from 'styled-components';
10
+ import { loadedImageUrl } from '../supportTicket/useLoadedImages';
10
11
 
11
12
  /*
12
13
  * The shared full-image viewer: a `ReqoreModal` holding one image at a time, with
@@ -121,6 +122,7 @@ export const ImageLightbox = ({
121
122
  }
122
123
  requested.current.add(id);
123
124
  fetchRef.current(id)
125
+ .then(loadedImageUrl)
124
126
  .then((resolved) => mounted.current && setUrls((current) => ({ ...current, [id]: resolved })))
125
127
  .catch(() => mounted.current && setUrls((current) => ({ ...current, [id]: null })));
126
128
  return undefined;
@@ -31,8 +31,9 @@ export interface IInterfaceReferenceTagsProps {
31
31
  */
32
32
  onRemove?: (reference: IInterfaceReference) => void;
33
33
  size?: TSizes;
34
- /** Chip styling — pass `intent`/`customTheme` (e.g. `{ main: 'custom1' }`) to tint
35
- * the reference chips to a surface's accent (the Qonsole/helpdesk composer does). */
34
+ /** Chip styling. Left out, a reference is a quiet tag with its kind's icon, as the
35
+ * support surfaces draw it; tint it only where the colour has a job (Qonsole's purple
36
+ * is Qonsole's alone — instruction-files decision 002). */
36
37
  intent?: ComponentProps<typeof ReqoreTag>['intent'];
37
38
  customTheme?: ComponentProps<typeof ReqoreTag>['customTheme'];
38
39
  /** Resolve a per-reference image — e.g. a qog's trigger-app logo, the way the
@@ -29,8 +29,6 @@ const meta = {
29
29
  export default meta;
30
30
  type Story = StoryObj<typeof meta>;
31
31
 
32
- const ACCENT = { main: 'custom1' } as const;
33
-
34
32
  /* The kind ids a host passes — wire values, not labels. `qog` is the customer-facing
35
33
  * kind for what the instance still lists as `fsm`; hosts map that at the fetch
36
34
  * boundary, so `fsm` never reaches the picker. */
@@ -143,7 +141,6 @@ const Hosted = ({
143
141
  their own remove — and read a size up from the thread's, where they're
144
142
  a record rather than a control. */
145
143
  <InterfaceReferenceTags
146
- customTheme={ACCENT}
147
144
  references={picked}
148
145
  onRemove={(reference) =>
149
146
  setPicked((current) =>
@@ -167,7 +164,6 @@ const Hosted = ({
167
164
  flat
168
165
  badge={picked.length || undefined}
169
166
  active={open}
170
- customTheme={ACCENT}
171
167
  tooltip={open ? 'Hide references' : 'Reference interfaces'}
172
168
  onClick={() => setOpen((shown) => !shown)}
173
169
  />