@qoretechnologies/reqraft 0.10.44-pr.95.gb67bfb7 → 0.10.44

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 (144) hide show
  1. package/.claude/CLAUDE.md +12 -0
  2. package/.tasks/INDEX.md +1 -0
  3. package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
  4. package/dist/components/composer/Composer.d.ts.map +1 -1
  5. package/dist/components/composer/Composer.js +5 -1
  6. package/dist/components/composer/Composer.js.map +1 -1
  7. package/dist/components/form/engine/CompactRow.d.ts +20 -1
  8. package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
  9. package/dist/components/form/engine/CompactRow.js +279 -41
  10. package/dist/components/form/engine/CompactRow.js.map +1 -1
  11. package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
  12. package/dist/components/form/engine/CompactToolbar.js +72 -72
  13. package/dist/components/form/engine/CompactToolbar.js.map +1 -1
  14. package/dist/components/form/engine/FormEngine.d.ts +48 -1
  15. package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
  16. package/dist/components/form/engine/FormEngine.js +203 -98
  17. package/dist/components/form/engine/FormEngine.js.map +1 -1
  18. package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
  19. package/dist/components/form/engine/_structuredData/SchemaDataView.js +108 -20
  20. package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
  21. package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
  22. package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
  23. package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
  24. package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
  25. package/dist/components/form/engine/compactRowStyles.d.ts +2 -0
  26. package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
  27. package/dist/components/form/engine/compactRowStyles.js +21 -4
  28. package/dist/components/form/engine/compactRowStyles.js.map +1 -1
  29. package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
  30. package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
  31. package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
  32. package/dist/components/form/engine/readFirst.d.ts +24 -0
  33. package/dist/components/form/engine/readFirst.d.ts.map +1 -1
  34. package/dist/components/form/engine/readFirst.js +84 -6
  35. package/dist/components/form/engine/readFirst.js.map +1 -1
  36. package/dist/components/form/fields/Field.d.ts.map +1 -1
  37. package/dist/components/form/fields/Field.js +1 -1
  38. package/dist/components/form/fields/Field.js.map +1 -1
  39. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
  40. package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
  41. package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
  42. package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
  43. package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
  44. package/dist/components/form/fields/auto/AutoFormField.js +64 -14
  45. package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
  46. package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
  47. package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
  48. package/dist/components/form/fields/long-string/LongString.js +25 -4
  49. package/dist/components/form/fields/long-string/LongString.js.map +1 -1
  50. package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
  51. package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
  52. package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
  53. package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
  54. package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
  55. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
  56. package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
  57. package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
  58. package/dist/components/form/fields/template/TemplateField.js +35 -6
  59. package/dist/components/form/fields/template/TemplateField.js.map +1 -1
  60. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
  61. package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
  62. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
  63. package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
  64. package/dist/components/form/index.d.ts +1 -0
  65. package/dist/components/form/index.d.ts.map +1 -1
  66. package/dist/components/form/index.js +1 -0
  67. package/dist/components/form/index.js.map +1 -1
  68. package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
  69. package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
  70. package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
  71. package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
  72. package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
  73. package/dist/components/ticketThread/TicketThread.js +1 -1
  74. package/dist/components/ticketThread/TicketThread.js.map +1 -1
  75. package/dist/helpers/common.d.ts +8 -1
  76. package/dist/helpers/common.d.ts.map +1 -1
  77. package/dist/helpers/common.js +9 -1
  78. package/dist/helpers/common.js.map +1 -1
  79. package/dist/helpers/optionUiTypes.d.ts +30 -0
  80. package/dist/helpers/optionUiTypes.d.ts.map +1 -0
  81. package/dist/helpers/optionUiTypes.js +148 -0
  82. package/dist/helpers/optionUiTypes.js.map +1 -0
  83. package/dist/helpers/options.d.ts +19 -0
  84. package/dist/helpers/options.d.ts.map +1 -1
  85. package/dist/helpers/options.js +25 -1
  86. package/dist/helpers/options.js.map +1 -1
  87. package/dist/helpers/providerValue.d.ts +68 -0
  88. package/dist/helpers/providerValue.d.ts.map +1 -0
  89. package/dist/helpers/providerValue.js +50 -0
  90. package/dist/helpers/providerValue.js.map +1 -0
  91. package/dist/helpers/singleLineString.d.ts +37 -0
  92. package/dist/helpers/singleLineString.d.ts.map +1 -0
  93. package/dist/helpers/singleLineString.js +66 -0
  94. package/dist/helpers/singleLineString.js.map +1 -0
  95. package/dist/helpers/templates.d.ts +100 -0
  96. package/dist/helpers/templates.d.ts.map +1 -1
  97. package/dist/helpers/templates.js +237 -4
  98. package/dist/helpers/templates.js.map +1 -1
  99. package/dist/helpers/validations.d.ts +103 -2
  100. package/dist/helpers/validations.d.ts.map +1 -1
  101. package/dist/helpers/validations.js +858 -91
  102. package/dist/helpers/validations.js.map +1 -1
  103. package/dist/index.d.ts +1 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2 -1
  106. package/dist/index.js.map +1 -1
  107. package/package.json +1 -1
  108. package/src/components/composer/Composer.tsx +7 -3
  109. package/src/components/form/engine/CompactRow.tsx +391 -29
  110. package/src/components/form/engine/CompactToolbar.tsx +105 -98
  111. package/src/components/form/engine/FormEngine.stories.tsx +863 -12
  112. package/src/components/form/engine/FormEngine.tsx +231 -68
  113. package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +123 -0
  114. package/src/components/form/engine/_structuredData/SchemaDataView.tsx +217 -95
  115. package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
  116. package/src/components/form/engine/compactRowStyles.ts +38 -1
  117. package/src/components/form/engine/compactToolbarContext.ts +8 -0
  118. package/src/components/form/engine/readFirst.ts +125 -35
  119. package/src/components/form/fields/Field.tsx +3 -1
  120. package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
  121. package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
  122. package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
  123. package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
  124. package/src/components/form/fields/long-string/LongString.tsx +46 -5
  125. package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
  126. package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
  127. package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
  128. package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
  129. package/src/components/form/fields/template/TemplateField.stories.tsx +314 -1
  130. package/src/components/form/fields/template/TemplateField.tsx +47 -8
  131. package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
  132. package/src/components/form/index.tsx +1 -0
  133. package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
  134. package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
  135. package/src/components/ticketThread/TicketThread.tsx +7 -0
  136. package/src/helpers/common.ts +11 -1
  137. package/src/helpers/optionUiTypes.ts +153 -0
  138. package/src/helpers/options.ts +23 -0
  139. package/src/helpers/providerValue.ts +125 -0
  140. package/src/helpers/singleLineString.ts +61 -0
  141. package/src/helpers/templates.ts +272 -3
  142. package/src/helpers/validations.ts +1177 -109
  143. package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
  144. package/src/index.tsx +1 -0
@@ -30,9 +30,11 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
30
30
  import { useUpdateEffect } from 'react-use';
31
31
  import {
32
32
  filterTemplatesByType as templatesFilterFunc,
33
- findTemplate,
34
33
  getTemplateKey,
35
34
  getTemplateValue,
35
+ isBracedTemplateToken,
36
+ isCompleteTemplateToken,
37
+ describeTemplateReference,
36
38
  isValueTemplate,
37
39
  } from '../../../../helpers/templates';
38
40
  import { getTypeFromValue } from '../../../../helpers/validations';
@@ -72,8 +74,17 @@ export const TemplatesListProps: IReqoreDropdownProps = {
72
74
  // markup stays verbatim. `type`/`level`/`allowTemplates` are destructured
73
75
  // only to keep them off the underlying ReQore component / DOM.
74
76
  /* eslint-disable @typescript-eslint/no-unused-vars */
77
+ // `type` is FORWARDED here, unlike the other wrappers below: the long-string
78
+ // field consumes it to decide whether it holds one line (a `string` does, a
79
+ // `long-string`, `hash` or `list` does not) and never spreads it to the DOM.
80
+ // Dropping it made every string field in every FormEngine form a growing
81
+ // textarea, so an alert rule's Internal Name took Enter.
75
82
  const LongStringField = ({ name, onChange, type, level, allowTemplates, ...rest }: any) => (
76
- <LongStringFormField {...rest} onChange={(value: string) => onChange?.(name, value)} />
83
+ <LongStringFormField
84
+ {...rest}
85
+ type={type}
86
+ onChange={(value: string) => onChange?.(name, value)}
87
+ />
77
88
  );
78
89
  const Number = ({ name, onChange, type, level, allowTemplates, ...rest }: any) => (
79
90
  <NumberFormField {...rest} onChange={(value: number | string) => onChange?.(name, value)} />
@@ -216,8 +227,13 @@ export const TemplateDropdownSelector = memo(
216
227
  size,
217
228
  ...rest
218
229
  }: ITemplateDropdownSelectorProps) => {
219
- const template = findTemplate(templates, value);
220
- const label = template?.label || value || rest.label || 'Select Template';
230
+ // One resolver for every surface that names a reference — the picker chip
231
+ // here, the read-only tag, and the compact row's expression summary. The
232
+ // row and this editor must agree: a reference that reads as its path when
233
+ // the row is closed cannot read as `$data:{…}` the moment it is opened.
234
+ const resolved = describeTemplateReference(templates, value);
235
+ const template = resolved.item;
236
+ const label = resolved.label || rest.label || 'Select Template';
221
237
  const leftIconProps = useMemo(
222
238
  (): IReqoreButtonProps['leftIconProps'] => ({
223
239
  image: template?.metadata?.image,
@@ -335,8 +351,15 @@ export const TemplateField = memo(
335
351
 
336
352
  useEffect(() => {
337
353
  if (!isTemplate && isValueTemplate(value) && allowTemplates) {
338
- // Do not set the template value if the value is a string in auto mode
339
- if (type === 'auto' && getTypeFromValue(value) === 'string') {
354
+ // In auto mode, leave user-typed dollar-strings alone ('$foo: hello'
355
+ // passes the loose check) — but a string that IS one well-formed token
356
+ // ($data:{…}, $config:item) must still flip into template mode, or an
357
+ // arg whose value hydrates after mount is stuck rendering raw text.
358
+ if (
359
+ type === 'auto' &&
360
+ getTypeFromValue(value) === 'string' &&
361
+ !isCompleteTemplateToken(value)
362
+ ) {
340
363
  return;
341
364
  }
342
365
 
@@ -359,6 +382,15 @@ export const TemplateField = memo(
359
382
 
360
383
  const showTemplateToggle = allowCustomValues && allowTemplates && !rest.arg_schema;
361
384
 
385
+ // Only a BRACED context ref (`$data:{…}` — machine-written, nobody types
386
+ // one) renders as the picker chip (named via `resolveTemplateLabel`)
387
+ // rather than as its raw text in a string editor. Plain word-path tokens
388
+ // (`$local:id`) are typeable, so per the build #123 review they keep the
389
+ // input that offers templates while typing. Mixed text-and-token strings
390
+ // keep the string editor too; the chip-in-editor treatment arrives with
391
+ // the rich-text string mode.
392
+ const templateValueIsBracedToken = isBracedTemplateToken(templateValue);
393
+
362
394
  const templateSupportsCustomValues =
363
395
  allowCustomValues && type === 'string' && !hasOnlyAllowedValues;
364
396
  const showTemplatesDropdown =
@@ -374,6 +406,11 @@ export const TemplateField = memo(
374
406
  showTemplatesDropdown;
375
407
 
376
408
  const Component = componentFromType ? ComponentMap[type] : Comp;
409
+ // Only a ComponentMap component gets `type`: it is ours and CONSUMES the
410
+ // prop (the long-string field needs it to know whether it holds one line)
411
+ // rather than spreading it onto the DOM. A caller-supplied `Comp` is someone
412
+ // else's, which is why `type` was kept off this call in the first place.
413
+ const componentTypeProp = componentFromType ? { type } : {};
377
414
  const fieldAriaLabel = rest['aria-label'] ?? label ?? rest.display_name ?? name;
378
415
 
379
416
  const filteredTemplates = useMemo<IReqoreFormTemplates>(():
@@ -746,13 +783,14 @@ export const TemplateField = memo(
746
783
  name={name}
747
784
  level={level}
748
785
  {...rest}
786
+ {...componentTypeProp}
749
787
  aria-label={fieldAriaLabel}
750
788
  className={`${className} template-selector`}
751
789
  templates={componentTemplates}
752
790
  />
753
791
  ) : null}
754
792
 
755
- {isTemplate && templateSupportsCustomValues ? (
793
+ {isTemplate && templateSupportsCustomValues && !templateValueIsBracedToken ? (
756
794
  <LongStringField
757
795
  className='template-selector'
758
796
  type='string'
@@ -769,7 +807,8 @@ export const TemplateField = memo(
769
807
  />
770
808
  ) : null}
771
809
 
772
- {showTemplatesDropdown ? (
810
+ {showTemplatesDropdown ||
811
+ (isTemplate && templateSupportsCustomValues && templateValueIsBracedToken) ? (
773
812
  <TemplateDropdownSelector
774
813
  allowCustomValues={allowCustomValues}
775
814
  templates={templates}
@@ -0,0 +1,7 @@
1
+ /* A deterministic "whole base64 file" example value — the payload class the
2
+ example-value modal exists for (an email attachment body). No spaces on
3
+ purpose: the unbroken run is what breaks naive rendering. The sentinel
4
+ proves a consumer holds the value to the very end. */
5
+ export const LONG_EXAMPLE_VALUE = `${'JVBERi0xLjcKJcTl8uXrp9Og0MTGCjQgMCBvYmoKPDwvRmlsdGVyL0ZsYXRlRGVjb2RlCg=='.repeat(
6
+ 90
7
+ )}THE_VERY_END`;
@@ -21,6 +21,7 @@ export * from './fields/rich-text/RichText';
21
21
  export * from './fields/select/Select';
22
22
  export * from './fields/select/SelectCollection';
23
23
  export * from './fields/string/String';
24
+ export * from './fields/template/ExampleValueModal';
24
25
  export * from './fields/template/TemplateField';
25
26
  export * from './engine/FormEngine';
26
27
  export * from './engine/optionActions';
@@ -0,0 +1,62 @@
1
+ import { StoryObj } from '@storybook/react-vite';
2
+ import { expect, fn, within } from 'storybook/test';
3
+
4
+ import { StoryMeta } from '../../types';
5
+ import { SearchFilterBar } from './SearchFilterBar';
6
+
7
+ /*
8
+ * The strip that sits above a list and narrows it: a search box plus the filter
9
+ * dropdowns that go with it.
10
+ */
11
+ const meta = {
12
+ component: SearchFilterBar,
13
+ title: 'Components/Search Filter Bar',
14
+ args: {
15
+ onChange: fn(),
16
+ placeholder: 'Search tickets…',
17
+ filters: [
18
+ {
19
+ icon: 'Filter3Line' as const,
20
+ label: 'Any status',
21
+ items: [
22
+ { label: 'Any status', value: 'any', selected: true },
23
+ { label: 'Open', value: 'open' },
24
+ { label: 'Resolved', value: 'resolved' },
25
+ ],
26
+ },
27
+ ],
28
+ },
29
+ } as StoryMeta<typeof SearchFilterBar>;
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ export const Idle: Story = {
35
+ args: { value: '' },
36
+ parameters: {
37
+ docs: {
38
+ description: {
39
+ story: 'Nothing typed — the bar is quiet, and the list below it is the whole list.',
40
+ },
41
+ },
42
+ },
43
+ play: async ({ canvasElement }) => {
44
+ expect(within(canvasElement).getByPlaceholderText('Search tickets…')).toBeVisible();
45
+ },
46
+ };
47
+
48
+ export const Filtering: Story = {
49
+ args: { value: 'deploy', intent: 'info' },
50
+ parameters: {
51
+ docs: {
52
+ description: {
53
+ story:
54
+ 'A query is active, so the box carries `intent="info"`. Without it a filtered list and a genuinely short list look identical, and a reader who lands on three rows cannot tell which they are looking at.',
55
+ },
56
+ },
57
+ },
58
+ play: async ({ canvasElement }) => {
59
+ const input = within(canvasElement).getByPlaceholderText('Search tickets…');
60
+ expect(input).toHaveValue('deploy');
61
+ },
62
+ };
@@ -1,4 +1,5 @@
1
1
  import { ReqoreControlGroup, ReqoreDropdown, ReqoreInput } from '@qoretechnologies/reqore';
2
+ import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
2
3
  import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
3
4
  import styled, { css } from 'styled-components';
4
5
 
@@ -70,6 +71,16 @@ export interface ISearchFilterBarProps {
70
71
  * panel (the References tab); a bar above a page-level list does not need it.
71
72
  */
72
73
  sticky?: boolean;
74
+ /**
75
+ * Intent for the search box.
76
+ *
77
+ * A bar that is narrowing the list should look different from one that is
78
+ * not — otherwise a reader who arrives at a short list cannot tell whether it
79
+ * is short or filtered. Callers pass `'info'` while a query is active. Left
80
+ * to the caller rather than derived from `value`, because a caller may hold
81
+ * other filter state this component never sees.
82
+ */
83
+ intent?: TReqoreIntent;
73
84
  }
74
85
 
75
86
  export const SearchFilterBar = ({
@@ -79,6 +90,7 @@ export const SearchFilterBar = ({
79
90
  filters = [],
80
91
  stacked,
81
92
  sticky,
93
+ intent,
82
94
  }: ISearchFilterBarProps) => (
83
95
  <Container $sticky={sticky}>
84
96
  {/* `size='small'` on the GROUP, not per control: the bar filters the content, it
@@ -98,6 +110,7 @@ export const SearchFilterBar = ({
98
110
  value={value}
99
111
  onChange={(event) => onChange((event.currentTarget as HTMLInputElement).value)}
100
112
  onClearClick={value ? () => onChange('') : undefined}
113
+ intent={intent}
101
114
  minimal
102
115
  fluid
103
116
  />
@@ -198,6 +198,13 @@ const StyledSkeletonThread = styled.div`
198
198
  gap: 12px;
199
199
  width: 100%;
200
200
  min-height: 100%;
201
+ /* A real message carries its own bubble, so it never touches the host's edge.
202
+ The skeleton is bare bars, and without this its avatars and lines were
203
+ pressed flat against the border of whatever holds the thread — which reads
204
+ as a clipped render rather than as loading. border-box so the padding comes
205
+ out of the 100% rather than adding to it. */
206
+ box-sizing: border-box;
207
+ padding: 12px;
201
208
  `;
202
209
 
203
210
  const StyledSkeletonRow = styled.div<{ $self: boolean }>`
@@ -1,5 +1,5 @@
1
1
  import { IReqoreRichTextEditorProps } from '@qoretechnologies/reqore/dist/components/RichTextEditor';
2
- import { TQorusFormFieldSchema } from '@qoretechnologies/ts-toolkit';
2
+ import { TQorusFormFieldSchema, TQorusFormOperatorValue } from '@qoretechnologies/ts-toolkit';
3
3
  import yaml from 'js-yaml';
4
4
 
5
5
  // ─── Typed ↔ YAML helpers ──────────────────────────────────────────────────────
@@ -163,6 +163,16 @@ export const richtextToSegments = (
163
163
  return segments;
164
164
  };
165
165
 
166
+ /**
167
+ * An operator value as the list it logically is.
168
+ *
169
+ * An option's `op` holds either one operator or a chain of them, and both
170
+ * spellings are legitimate on the wire. Every reader wants the list.
171
+ */
172
+ export const fixOperatorValue = (
173
+ operator: TQorusFormOperatorValue
174
+ ): (string | null | undefined)[] => (Array.isArray(operator) ? operator : [operator]);
175
+
166
176
  export const insertAtIndex = (array: any[] = [], index = 0, value: any): any[] => {
167
177
  return [...array.slice(0, index), value, ...array.slice(index)];
168
178
  };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The Qorus option `ui_type` vocabulary.
3
+ *
4
+ * A `ui_type` names the widget an option is rendered with, and — because the
5
+ * widget decides what a complete value looks like — it is also what
6
+ * `validateField` switches on. Both readers have to agree about which strings
7
+ * are in the vocabulary, so the lists live here rather than beside either one.
8
+ *
9
+ * Grouped by what the group means to a reader, not by alphabet: the scalar
10
+ * types hold a single value, the collection types hold several, the choice
11
+ * types pick from a supplied set, and the interface types name another Qorus
12
+ * object by its name — which is why `isOptionInterfaceUiType` is enough for
13
+ * validation to treat the whole group as a non-empty string.
14
+ */
15
+
16
+ export const OPTION_SCALAR_UI_TYPES = [
17
+ 'string',
18
+ 'long-string',
19
+ 'data',
20
+ 'binary',
21
+ 'richtext',
22
+ 'bool',
23
+ 'boolean',
24
+ 'date',
25
+ 'int',
26
+ 'integer',
27
+ 'float',
28
+ 'number',
29
+ 'byte-size',
30
+ 'url',
31
+ 'code-editor',
32
+ 'rgbcolor',
33
+ 'any',
34
+ 'auto',
35
+ // semantic string formats (string-backed types that carry a UI-renderable identity; see the qore
36
+ // DataProvider QoreStringFormatDataType)
37
+ 'email',
38
+ 'uri',
39
+ 'uuid',
40
+ 'hostname',
41
+ 'ipv4',
42
+ 'ipv6',
43
+ 'phone',
44
+ ] as const;
45
+
46
+ export const OPTION_COLLECTION_UI_TYPES = [
47
+ 'hash',
48
+ 'free-hash',
49
+ 'list',
50
+ 'free-list',
51
+ 'option_hash',
52
+ 'file',
53
+ 'file-as-string',
54
+ 'collection-documents',
55
+ ] as const;
56
+
57
+ export const OPTION_CHOICE_UI_TYPES = [
58
+ 'enum',
59
+ 'select-string',
60
+ 'select-array',
61
+ 'multi-select',
62
+ ] as const;
63
+
64
+ export const OPTION_INTERFACE_UI_TYPES = [
65
+ 'workflow',
66
+ 'service',
67
+ 'job',
68
+ 'connection',
69
+ 'mapper',
70
+ 'class',
71
+ 'value-map',
72
+ 'fsm',
73
+ 'qog',
74
+ 'api-schema',
75
+ 'auth-profile',
76
+ 'ai-collection',
77
+ 'ai-endpoint',
78
+ 'ai-guardrail',
79
+ 'ml-model',
80
+ 'release-config',
81
+ 'alertrule',
82
+ 'alertsilence',
83
+ 'installed-release',
84
+ 'business-process',
85
+ 'schema',
86
+ 'test',
87
+ ] as const;
88
+
89
+ export const OPTION_CUSTOM_UI_TYPES = [
90
+ 'processor',
91
+ 'processor-mappings',
92
+ 'data-provider',
93
+ 'tool-catalog',
94
+ 'schema-definition',
95
+ 'test-cases',
96
+ 'test-value-contract',
97
+ 'dpql',
98
+ 'active-windows',
99
+ ] as const;
100
+
101
+ export const OPTION_UI_TYPES = [
102
+ ...OPTION_SCALAR_UI_TYPES,
103
+ ...OPTION_COLLECTION_UI_TYPES,
104
+ ...OPTION_CHOICE_UI_TYPES,
105
+ ...OPTION_INTERFACE_UI_TYPES,
106
+ ...OPTION_CUSTOM_UI_TYPES,
107
+ ] as const;
108
+
109
+ export type TOptionUiType = (typeof OPTION_UI_TYPES)[number];
110
+ export type TOptionInterfaceUiType = (typeof OPTION_INTERFACE_UI_TYPES)[number];
111
+
112
+ export const LEGACY_FIELD_UI_TYPES = [
113
+ 'api-manager',
114
+ 'array',
115
+ 'array-auto',
116
+ 'array-of-pairs',
117
+ 'class-array',
118
+ 'class-connectors',
119
+ 'constant-array',
120
+ 'context-selector',
121
+ 'cron',
122
+ 'data-provider-options',
123
+ 'file-array',
124
+ 'fsm-list',
125
+ 'function-array',
126
+ 'http-content',
127
+ 'method-name',
128
+ 'opcua-method-arguments',
129
+ 'opcua-write-values',
130
+ 'options',
131
+ 'select',
132
+ 'service-events',
133
+ 'service-webhooks',
134
+ 'type-selector',
135
+ ] as const;
136
+
137
+ export const KNOWN_QORUS_UI_TYPES = [...OPTION_UI_TYPES, ...LEGACY_FIELD_UI_TYPES] as const;
138
+
139
+ export type TLegacyFieldUiType = (typeof LEGACY_FIELD_UI_TYPES)[number];
140
+ export type TKnownQorusUiType = (typeof KNOWN_QORUS_UI_TYPES)[number];
141
+
142
+ const optionUiTypeSet = new Set<string>(OPTION_UI_TYPES);
143
+ const interfaceUiTypeSet = new Set<string>(OPTION_INTERFACE_UI_TYPES);
144
+ const knownQorusUiTypeSet = new Set<string>(KNOWN_QORUS_UI_TYPES);
145
+
146
+ export const isOptionUiType = (type: unknown): type is TOptionUiType =>
147
+ typeof type === 'string' && optionUiTypeSet.has(type);
148
+
149
+ export const isOptionInterfaceUiType = (type: unknown): type is TOptionInterfaceUiType =>
150
+ typeof type === 'string' && interfaceUiTypeSet.has(type);
151
+
152
+ export const isKnownQorusUiType = (type: unknown): type is TKnownQorusUiType =>
153
+ typeof type === 'string' && knownQorusUiTypeSet.has(type);
@@ -29,3 +29,26 @@ export const getRequiredOptionMessage = (
29
29
 
30
30
  return `This field or ${requiredOptions} is required`;
31
31
  };
32
+
33
+ /**
34
+ * The value of one element of a list field.
35
+ *
36
+ * A list element arrives in either of two shapes, and both are legitimate. The
37
+ * editor keeps each element in a `{value, type}` envelope while the form is
38
+ * being worked on — that is what `formatToServerValue` writes — while a value
39
+ * read back from storage holds what the server contract asks for, which for a
40
+ * list of strings is the bare strings.
41
+ *
42
+ * Reaching straight for `.value` answered `undefined` for every stored element.
43
+ * On a read that emptied the row; on a validation pass it failed the element as
44
+ * empty; and on an autosaving form the emptiness was then written back over the
45
+ * stored value. One helper so the readers cannot disagree about which shape
46
+ * they are looking at.
47
+ *
48
+ * An array is never an envelope — a rich-text document is itself an array, and
49
+ * `'value' in []` is false anyway, but saying so keeps the intent legible.
50
+ */
51
+ export const getListElementValue = (element: unknown): unknown =>
52
+ element && typeof element === 'object' && !Array.isArray(element) && 'value' in element ?
53
+ (element as { value: unknown }).value
54
+ : element;
@@ -0,0 +1,125 @@
1
+ import { TQorusForm } from '@qoretechnologies/ts-toolkit';
2
+
3
+ export type TRecordType = 'search' | 'search-single' | 'create' | 'update' | 'delete';
4
+ export type TRealRecordType = 'read' | 'create' | 'update' | 'delete';
5
+
6
+ export type TProviderTypeSupports = {
7
+ [key in `supports_${TRealRecordType}`]?: boolean;
8
+ };
9
+
10
+ export type TProviderTypeArgs = {
11
+ [key in `${TRecordType}_args`]?: TQorusForm | TQorusForm[];
12
+ };
13
+
14
+ /**
15
+ * A data-provider selection, in the shape the form holds it.
16
+ *
17
+ * The `supports_*` flags say which record operations the selected provider can
18
+ * perform; validation reads them to decide whether a provider is a legitimate
19
+ * target for the operation the field was configured for.
20
+ */
21
+ export interface IProviderType extends TProviderTypeSupports, TProviderTypeArgs {
22
+ type: string;
23
+ name: string;
24
+ path?: string;
25
+ options?: TQorusForm;
26
+ subtype?: 'request' | 'response';
27
+ hasApiContext?: boolean;
28
+ optionsChanged?: boolean;
29
+ searchOptionsChanged?: boolean;
30
+ desc?: string;
31
+ use_args?: boolean;
32
+ args?: any;
33
+ up?: boolean;
34
+ supports_request?: boolean;
35
+ supports_messages?: 'ASYNC' | 'SYNC' | 'NONE';
36
+ supports_observable?: boolean;
37
+ transaction_management?: boolean;
38
+ record_requires_search_options?: boolean;
39
+ create_args_freeform?: TQorusForm[];
40
+ is_api_call?: boolean;
41
+ search_options?: TQorusForm;
42
+ descriptions?: string[];
43
+ message_id?: string;
44
+ message?: {
45
+ type: string;
46
+ value: any;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * A provider value as an object, from either representation a caller may hold.
52
+ *
53
+ * A provider reaches the form either already expanded into `IProviderType`, or
54
+ * as the flat path string the server stores — `"factory/db{driver=pgsql}/table"`
55
+ * or the plain `"type/name/path"`. Returns `null` for anything that is neither,
56
+ * so a caller reports an invalid value rather than reading fields off nothing.
57
+ */
58
+ export const maybeBuildOptionProvider = (provider: unknown): IProviderType | null => {
59
+ if (!provider) {
60
+ return null;
61
+ }
62
+ if (typeof provider === 'object') {
63
+ return provider as IProviderType;
64
+ }
65
+ if (typeof provider !== 'string') {
66
+ return null;
67
+ }
68
+
69
+ if (provider.startsWith('factory')) {
70
+ const fixedProvider = provider.endsWith('/') ? provider : `${provider}/`;
71
+ const [factoryType] = fixedProvider.split('/');
72
+ const factoryName = fixedProvider.substring(
73
+ fixedProvider.indexOf('/') + 1,
74
+ fixedProvider.lastIndexOf('{')
75
+ );
76
+ const options = fixedProvider.substring(
77
+ fixedProvider.indexOf('{') + 1,
78
+ fixedProvider.lastIndexOf('}')
79
+ );
80
+ const optionsObject = options
81
+ .split(',')
82
+ .filter(Boolean)
83
+ .reduce<Record<string, string>>((result, option) => {
84
+ const [key, value] = option.split('=');
85
+ result[key] = value;
86
+ return result;
87
+ }, {});
88
+ const result: IProviderType = {
89
+ type: factoryType,
90
+ name: factoryName,
91
+ path: fixedProvider.substring(fixedProvider.lastIndexOf('}/') + 2),
92
+ options: optionsObject as unknown as TQorusForm,
93
+ };
94
+ if (provider.includes('?options_changed')) {
95
+ result.optionsChanged = true;
96
+ }
97
+ return result;
98
+ }
99
+
100
+ const [type, name, ...path] = provider.split('/');
101
+ return { type, name, path: path.join('/') };
102
+ };
103
+
104
+ /**
105
+ * An FSM variable action — which variable, and what is done to it.
106
+ *
107
+ * The provider half is `Partial<IProviderType>` because the action's own
108
+ * `action_type` decides which provider fields have to be there; validation
109
+ * re-runs the matching provider validator over the merged value rather than
110
+ * duplicating those rules here.
111
+ */
112
+ export type TVariableActionValue = {
113
+ var_type: 'globalvar' | 'localvar' | 'autovar';
114
+ var_name: string;
115
+ transaction_action?: 'commit' | 'rollback' | 'begin-transaction';
116
+ action_type?:
117
+ | 'search'
118
+ | 'search-single'
119
+ | 'update'
120
+ | 'create'
121
+ | 'delete'
122
+ | 'transaction'
123
+ | 'send-message'
124
+ | 'apicall';
125
+ } & Partial<IProviderType>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which string field types hold one line, and how to keep them that way.
3
+ *
4
+ * The server already draws this distinction and the IDE was throwing it away:
5
+ * a service's `name` is declared `ui_type: "string"` while its `description` is
6
+ * `ui_type: "long-string"` with `markdown: True` (see `ServiceMetadata.qc`).
7
+ * Every one of them — plus `email`, `uuid`, `hostname`, `ipv4`, `ipv6` and
8
+ * `phone` — rendered through the same growing textarea, so a technical name, a
9
+ * version and an IP address all accepted Enter.
10
+ *
11
+ * That is not a cosmetic problem. These values become YAML keys, class names,
12
+ * URLs and identifiers; a newline in one of them is invalid input that the form
13
+ * happily accepted and passed on, and the operator had no way to see it.
14
+ *
15
+ * The rule is the server's own contract, stated once here rather than guessed
16
+ * per field, and it is enforced in `LongString.tsx`.
17
+ *
18
+ * It lives in reqraft rather than in a consumer because the consumer is not the
19
+ * only renderer: qorus-ide fixed this in its OWN long-string field, and every
20
+ * form that goes through FormEngine — which is where an alert rule's Internal
21
+ * Name, Name and Short Description are — reaches THIS one instead. The fix was
22
+ * real and the field in front of the operator still took Enter.
23
+ */
24
+
25
+ /**
26
+ * The string types that hold exactly one line — the ones routed to a textarea
27
+ * today.
28
+ *
29
+ * Listed explicitly rather than derived by excluding the document types,
30
+ * because the two directions fail very differently. Missing a single-line type
31
+ * here leaves it behaving as it does now — no worse. Wrongly treating a new
32
+ * DOCUMENT type as single-line would flatten a user's text as they typed it,
33
+ * which is data loss. So an unrecognised type stays multi-line.
34
+ */
35
+ const SINGLE_LINE_STRING_TYPES = new Set([
36
+ 'string',
37
+ 'email',
38
+ 'uuid',
39
+ 'hostname',
40
+ 'ipv4',
41
+ 'ipv6',
42
+ 'phone',
43
+ ]);
44
+
45
+ /** True when a string field of this type holds a single line. */
46
+ export const isSingleLineStringType = (type?: string): boolean =>
47
+ !!type && SINGLE_LINE_STRING_TYPES.has(type);
48
+
49
+ /**
50
+ * Flattens any line break to a single space.
51
+ *
52
+ * A space rather than nothing, because the newline is usually where a word
53
+ * boundary was — pasting two lines into a name should read as two words, not
54
+ * one run-together one. CR, LF and CRLF all collapse to one space, and the
55
+ * result is trimmed of leading/trailing whitespace the break introduced.
56
+ */
57
+ export const flattenToSingleLine = (value: string): string =>
58
+ value.replace(/\r\n|\r|\n/g, ' ').replace(/ {2,}/g, ' ');
59
+
60
+ /** True when the value carries a line break this field should not hold. */
61
+ export const hasLineBreak = (value: string): boolean => /\r|\n/.test(value);