@qoretechnologies/reqraft 0.10.46-pr.95.ge595a13 → 0.10.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +12 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/TEMPLATE_TOKEN_GRAMMAR.md +72 -0
- package/dist/components/composer/Composer.d.ts.map +1 -1
- package/dist/components/composer/Composer.js +5 -1
- package/dist/components/composer/Composer.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts +14 -0
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +199 -41
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +72 -72
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +65 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +276 -108
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +55 -13
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.d.ts +15 -0
- package/dist/components/form/engine/_structuredData/structuredData.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/structuredData.js +22 -1
- package/dist/components/form/engine/_structuredData/structuredData.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +15 -3
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +8 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +24 -0
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +84 -6
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +1 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +12 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +16 -6
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +64 -14
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +9 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +25 -4
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/template/ExampleValueModal.d.ts +29 -0
- package/dist/components/form/fields/template/ExampleValueModal.d.ts.map +1 -0
- package/dist/components/form/fields/template/ExampleValueModal.js +122 -0
- package/dist/components/form/fields/template/ExampleValueModal.js.map +1 -0
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -3
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +81 -27
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts +2 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.d.ts.map +1 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js +9 -0
- package/dist/components/form/fields/template/__fixtures__/longExampleValue.js.map +1 -0
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts +12 -1
- package/dist/components/searchFilterBar/SearchFilterBar.d.ts.map +1 -1
- package/dist/components/searchFilterBar/SearchFilterBar.js +2 -2
- package/dist/components/searchFilterBar/SearchFilterBar.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +1 -1
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/common.d.ts +8 -1
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +9 -1
- package/dist/helpers/common.js.map +1 -1
- package/dist/helpers/expressions.d.ts.map +1 -1
- package/dist/helpers/expressions.js +15 -2
- package/dist/helpers/expressions.js.map +1 -1
- package/dist/helpers/optionUiTypes.d.ts +30 -0
- package/dist/helpers/optionUiTypes.d.ts.map +1 -0
- package/dist/helpers/optionUiTypes.js +156 -0
- package/dist/helpers/optionUiTypes.js.map +1 -0
- package/dist/helpers/options.d.ts +19 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +25 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/providerValue.d.ts +68 -0
- package/dist/helpers/providerValue.d.ts.map +1 -0
- package/dist/helpers/providerValue.js +50 -0
- package/dist/helpers/providerValue.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts +37 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -0
- package/dist/helpers/singleLineString.js +66 -0
- package/dist/helpers/singleLineString.js.map +1 -0
- package/dist/helpers/templates.d.ts +100 -0
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +237 -4
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts +103 -2
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +858 -91
- package/dist/helpers/validations.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/composer/Composer.tsx +7 -3
- package/src/components/form/engine/CompactRow.tsx +279 -33
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +995 -12
- package/src/components/form/engine/FormEngine.tsx +377 -94
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +104 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +158 -101
- package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
- package/src/components/form/engine/compactRowStyles.ts +55 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +125 -35
- package/src/components/form/fields/Field.tsx +3 -1
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +24 -2
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +81 -0
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +47 -1
- package/src/components/form/fields/auto/AutoFormField.tsx +73 -11
- package/src/components/form/fields/long-string/LongString.tsx +46 -5
- package/src/components/form/fields/schema-definition/SchemaDefinitionField.stories.tsx +5 -0
- package/src/components/form/fields/template/ExampleValueModal.stories.tsx +114 -0
- package/src/components/form/fields/template/ExampleValueModal.tsx +99 -0
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +8 -4
- package/src/components/form/fields/template/TemplateField.stories.tsx +389 -1
- package/src/components/form/fields/template/TemplateField.tsx +100 -35
- package/src/components/form/fields/template/__fixtures__/longExampleValue.ts +7 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/searchFilterBar/SearchFilterBar.stories.tsx +62 -0
- package/src/components/searchFilterBar/SearchFilterBar.tsx +13 -0
- package/src/components/ticketThread/TicketThread.tsx +7 -0
- package/src/helpers/common.ts +11 -1
- package/src/helpers/expressions.ts +16 -2
- package/src/helpers/optionUiTypes.ts +161 -0
- package/src/helpers/options.ts +23 -0
- package/src/helpers/providerValue.ts +125 -0
- package/src/helpers/singleLineString.ts +61 -0
- package/src/helpers/templates.ts +272 -3
- package/src/helpers/validations.ts +1177 -109
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +23 -2
- package/src/index.tsx +1 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
TQorusType,
|
|
6
6
|
} from '@qoretechnologies/ts-toolkit';
|
|
7
7
|
import { isRendererOnlyUiType } from './rendererTypes';
|
|
8
|
-
import { isUiEncodedValue } from './_structuredData/structuredData';
|
|
8
|
+
import { isEmptyUiEnvelope, isUiEncodedValue } from './_structuredData/structuredData';
|
|
9
9
|
import { renderExpressionToText } from '../expressions/renderExpressionToText';
|
|
10
10
|
import { IExpressionValue } from '../expressions/types';
|
|
11
11
|
import yaml from 'js-yaml';
|
|
@@ -155,6 +155,45 @@ export const isOptionValueEmpty = (value: unknown): boolean =>
|
|
|
155
155
|
value === '' ||
|
|
156
156
|
(Array.isArray(value) && value.length === 0);
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* A schema-declared hash whose every entry is a materialised-but-unset field.
|
|
160
|
+
*
|
|
161
|
+
* `preselected` on a sub-field means "show this in the form so the author sees
|
|
162
|
+
* it exists" — a FORM instruction. Opening the sub-form materialises each one,
|
|
163
|
+
* `fixOptions` writes `{type: 'int'}` with no value, and the draft autosaves
|
|
164
|
+
* that, so the stored hash permanently carries keys that stand for nothing.
|
|
165
|
+
*
|
|
166
|
+
* The row then read those keys as content: `Runtime Defaults` listed three
|
|
167
|
+
* sub-fields with no values, counted toward "7/12 set", and showed a green
|
|
168
|
+
* set-dot — while the sub-form it summarises correctly said 0/3 set. Nothing
|
|
169
|
+
* was ever entered.
|
|
170
|
+
*
|
|
171
|
+
* Requires the sub-schema, and requires each key to be declared by it: without
|
|
172
|
+
* that, `{type: 'x'}` could be somebody's data rather than an empty envelope.
|
|
173
|
+
*/
|
|
174
|
+
export const isUnsetSchemaHash = (value: unknown, schema?: TQorusFormFieldSchema): boolean => {
|
|
175
|
+
const argSchema = (schema as { arg_schema?: Record<string, unknown> } | undefined)?.arg_schema;
|
|
176
|
+
if (!argSchema || typeof argSchema !== 'object') {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const inner =
|
|
180
|
+
value && typeof value === 'object' && !Array.isArray(value) && 'value' in (value as object) ?
|
|
181
|
+
(value as { value: unknown }).value
|
|
182
|
+
: value;
|
|
183
|
+
if (!inner || typeof inner !== 'object' || Array.isArray(inner)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const keys = Object.keys(inner as object);
|
|
187
|
+
return (
|
|
188
|
+
keys.length > 0 &&
|
|
189
|
+
keys.every(
|
|
190
|
+
(key) =>
|
|
191
|
+
!!(argSchema as Record<string, unknown>)[key] &&
|
|
192
|
+
isEmptyUiEnvelope((inner as Record<string, unknown>)[key])
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
158
197
|
export const isFixedCompactAllowedValueOption = (
|
|
159
198
|
schema: TQorusFormFieldSchema | undefined
|
|
160
199
|
): boolean => {
|
|
@@ -200,6 +239,13 @@ export const shouldAutoCollapseCompactOption = (
|
|
|
200
239
|
* Prefer the matching allowed_values entry's display_name (fallback `name`)
|
|
201
240
|
* over the raw stored value.
|
|
202
241
|
*
|
|
242
|
+
* A LIST carries its options under `element_allowed_values` — they constrain
|
|
243
|
+
* each element, not the list itself — so a multi-select that was missing here
|
|
244
|
+
* printed what it stores rather than what you picked: `orders, batch` for
|
|
245
|
+
* fields whose picker reads "Orders, Batch". The gap shows worst exactly where
|
|
246
|
+
* allowed values earn their keep, since the stored form is often not readable
|
|
247
|
+
* at all (a permission code, `PO_REQUIRE_TYPES`, an app-specific id).
|
|
248
|
+
*
|
|
203
249
|
* Exported (as `findAllowedValueOption`) because the form engine's value
|
|
204
250
|
* validation has to answer the SAME question — "is this stored value one of the
|
|
205
251
|
* declared choices?" — and answering it differently loses data. An
|
|
@@ -215,13 +261,30 @@ export const findAllowedValueOption = (
|
|
|
215
261
|
value: unknown,
|
|
216
262
|
schema?: TQorusFormFieldSchema
|
|
217
263
|
): any | undefined => {
|
|
218
|
-
const s = schema as
|
|
219
|
-
|
|
264
|
+
const s = schema as
|
|
265
|
+
{ allowed_values?: any[]; element_allowed_values?: any[]; items?: any[] } | undefined;
|
|
266
|
+
const options =
|
|
267
|
+
(s?.allowed_values?.length && s.allowed_values) ||
|
|
268
|
+
(s?.element_allowed_values?.length && s.element_allowed_values) ||
|
|
269
|
+
s?.items;
|
|
220
270
|
if (!options?.length) {
|
|
221
271
|
return undefined;
|
|
222
272
|
}
|
|
273
|
+
// A stored element can be the bare value or the typed `{type, value}`
|
|
274
|
+
// envelope the form engine round-trips; match either against either.
|
|
275
|
+
const stored =
|
|
276
|
+
value && typeof value === 'object' && 'value' in (value as Record<string, unknown>) ?
|
|
277
|
+
(value as Record<string, unknown>).value
|
|
278
|
+
: value;
|
|
279
|
+
|
|
223
280
|
return options.find(
|
|
224
|
-
(option) =>
|
|
281
|
+
(option) =>
|
|
282
|
+
option?.value?.value === value ||
|
|
283
|
+
option?.value === value ||
|
|
284
|
+
option?.name === value ||
|
|
285
|
+
option?.value?.value === stored ||
|
|
286
|
+
option?.value === stored ||
|
|
287
|
+
option?.name === stored
|
|
225
288
|
);
|
|
226
289
|
};
|
|
227
290
|
|
|
@@ -499,7 +562,7 @@ export const formatOptionValue = (
|
|
|
499
562
|
): string => {
|
|
500
563
|
const value = option?.value;
|
|
501
564
|
|
|
502
|
-
if (isOptionValueEmpty(value)) {
|
|
565
|
+
if (isOptionValueEmpty(value) || isUnsetSchemaHash(value, schema)) {
|
|
503
566
|
return '';
|
|
504
567
|
}
|
|
505
568
|
|
|
@@ -641,34 +704,55 @@ export const getHashEntries = (
|
|
|
641
704
|
const argSchema = (schema as { arg_schema?: Record<string, TQorusFormFieldSchema> } | undefined)
|
|
642
705
|
?.arg_schema;
|
|
643
706
|
|
|
644
|
-
return Object.keys(hashObject)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
707
|
+
return Object.keys(hashObject)
|
|
708
|
+
.filter((key) => {
|
|
709
|
+
// Only sub-fields with a value belong in a read view. `preselected`
|
|
710
|
+
// materialises a field so the FORM shows it; that is not a statement
|
|
711
|
+
// that the object has one, and listing it here says the opposite of
|
|
712
|
+
// what the sub-form says.
|
|
713
|
+
const subSchema = argSchema?.[key];
|
|
714
|
+
return !(subSchema && isEmptyUiEnvelope(hashObject![key]));
|
|
715
|
+
})
|
|
716
|
+
.map((key) => {
|
|
717
|
+
const raw = hashObject![key];
|
|
718
|
+
const subSchema = argSchema?.[key];
|
|
719
|
+
// A schema-declared (`arg_schema`) hash stores each entry as a
|
|
720
|
+
// `{ type, value }` field descriptor — gate on the sub-schema's presence.
|
|
721
|
+
// A schema-less hash *usually* stores raw values, but the server's typed
|
|
722
|
+
// serialization can nest envelopes there too (e.g. a hash default_value
|
|
723
|
+
// whose entries are `{ type: 'string', value: 'x' }`) — recognise those by
|
|
724
|
+
// their strict shape (a string `type`, a `value`, and no foreign keys) so
|
|
725
|
+
// the sub-row shows 'x' rather than counting the envelope's own keys.
|
|
726
|
+
// `'value' in raw` is FALSE for a materialised-but-unset field: `fixOptions`
|
|
727
|
+
// omits the key entirely when the value is undefined, leaving `{type: 'x'}`.
|
|
728
|
+
// Requiring `value` therefore classed that envelope as a raw hash VALUE and
|
|
729
|
+
// counted its own `type` key, so an untouched Runtime Defaults collapsed to
|
|
730
|
+
// "Timeout (Seconds) 1 field" three times over while the sub-form correctly
|
|
731
|
+
// said 0/3 set. With a sub-schema present, a `type`-only object is an empty
|
|
732
|
+
// envelope, never content.
|
|
733
|
+
// Shared with SchemaDataView, which previews this same value one level
|
|
734
|
+
// down: two renderers disagreeing about what an unset field looks like is
|
|
735
|
+
// how the row said "1 field" long after this one stopped.
|
|
736
|
+
const isEmptyEnvelope = !!subSchema && isEmptyUiEnvelope(raw);
|
|
737
|
+
const isFieldShape =
|
|
738
|
+
(!!subSchema &&
|
|
739
|
+
!!raw &&
|
|
740
|
+
typeof raw === 'object' &&
|
|
741
|
+
!Array.isArray(raw) &&
|
|
742
|
+
'value' in (raw as object)) ||
|
|
743
|
+
isEmptyEnvelope ||
|
|
744
|
+
isTypedEnvelope(raw);
|
|
745
|
+
const subOption: IQorusFormField =
|
|
746
|
+
isFieldShape ?
|
|
747
|
+
(raw as IQorusFormField)
|
|
748
|
+
: ({ type: subSchema?.type as IQorusFormField['type'], value: raw } as IQorusFormField);
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
name: key,
|
|
752
|
+
label: subSchema?.display_name || key,
|
|
753
|
+
value: formatOptionValue(subOption, subSchema),
|
|
754
|
+
};
|
|
755
|
+
});
|
|
672
756
|
};
|
|
673
757
|
|
|
674
758
|
const titleCase = (value: string): string =>
|
|
@@ -823,7 +907,10 @@ export const isSet = (value: unknown): boolean =>
|
|
|
823
907
|
* (that is the order the form puts the fields in), then anything stored that the
|
|
824
908
|
* schema does not mention, so undescribed data is shown rather than dropped.
|
|
825
909
|
*/
|
|
826
|
-
export const orderedKeys = (
|
|
910
|
+
export const orderedKeys = (
|
|
911
|
+
record: Record<string, unknown>,
|
|
912
|
+
schema: IQorusFormSchema
|
|
913
|
+
): string[] => {
|
|
827
914
|
const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
|
|
828
915
|
const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
|
|
829
916
|
return [...described, ...extra];
|
|
@@ -904,7 +991,10 @@ export const recordIdentity = (
|
|
|
904
991
|
|
|
905
992
|
return {
|
|
906
993
|
key,
|
|
907
|
-
text: formatOptionValue(
|
|
994
|
+
text: formatOptionValue(
|
|
995
|
+
{ type: fieldSchema?.type, value: raw } as IQorusFormField,
|
|
996
|
+
fieldSchema
|
|
997
|
+
),
|
|
908
998
|
label: fieldLabel(key, fieldSchema),
|
|
909
999
|
mono: !findAllowedValueOption(raw, fieldSchema),
|
|
910
1000
|
};
|
|
@@ -569,7 +569,9 @@ export const FormField = <T extends TFormFieldType>({
|
|
|
569
569
|
return (
|
|
570
570
|
<ReqoreControlGroup vertical size='tiny'>
|
|
571
571
|
<ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
|
|
572
|
-
Select one:
|
|
572
|
+
{/* Headed by the field's own name; "Select one:" only when it has
|
|
573
|
+
none. Same rule as FieldAllowedValuesCheckGroup. */}
|
|
574
|
+
{display_name ? `${display_name}:` : 'Select one:'}
|
|
573
575
|
</ReqoreSpan>
|
|
574
576
|
{allowed_values.map((item, index) => {
|
|
575
577
|
const itemValue = item.value?.value;
|
|
@@ -43,15 +43,28 @@ export const FieldAllowedValuesCheckGroup = memo(
|
|
|
43
43
|
onChange,
|
|
44
44
|
name,
|
|
45
45
|
value,
|
|
46
|
+
label,
|
|
46
47
|
...rest
|
|
47
48
|
}: Partial<Omit<IFieldAllowedValuesProps, 'items'>> & {
|
|
48
49
|
multiSelect?: boolean;
|
|
49
50
|
items: ISelectFormFieldItem[];
|
|
51
|
+
/**
|
|
52
|
+
* The field's own name, used as the group's heading.
|
|
53
|
+
*
|
|
54
|
+
* A picker headed by what it IS beats one headed by a generic instruction:
|
|
55
|
+
* "Select one:" says nothing the shape of a radio group has not already
|
|
56
|
+
* said, and in a container that absorbs a sibling it forced the field's name
|
|
57
|
+
* to be printed a second time beside the group. Falls back to the
|
|
58
|
+
* instruction when a field declares no display name.
|
|
59
|
+
*/
|
|
60
|
+
label?: string;
|
|
50
61
|
}) => {
|
|
51
62
|
return (
|
|
52
63
|
<ReqoreControlGroup vertical size={rest.size || 'small'} gapSize='tiny'>
|
|
53
64
|
<ReqoreSpan effect={{ opacity: 0.6, uppercase: true, weight: 'bold' }} size='tiny'>
|
|
54
|
-
{
|
|
65
|
+
{label ? `${label}:`
|
|
66
|
+
: multiSelect ? 'Select one or more:'
|
|
67
|
+
: 'Select one:'}
|
|
55
68
|
</ReqoreSpan>
|
|
56
69
|
{items?.map((item) => (
|
|
57
70
|
<ReqoreCheckbox
|
|
@@ -62,10 +75,17 @@ export const FieldAllowedValuesCheckGroup = memo(
|
|
|
62
75
|
disabled={rest.disabled}
|
|
63
76
|
readOnly={rest.readOnly}
|
|
64
77
|
intent={item.value === value ? 'info' : undefined}
|
|
78
|
+
// `isEqual`, and only once there is a value to match. The single
|
|
79
|
+
// -select branch compared JSON.stringify(value) with
|
|
80
|
+
// JSON.stringify(item.value), and JSON.stringify(undefined) is
|
|
81
|
+
// undefined — so an unset field whose items carry no resolved value
|
|
82
|
+
// compared undefined with undefined and reported EVERY option as
|
|
83
|
+
// checked, on a field that was simultaneously "This field is
|
|
84
|
+
// required". Nothing selected must read as nothing selected.
|
|
65
85
|
checked={
|
|
66
86
|
multiSelect
|
|
67
87
|
? Array.isArray(value) && value.some((v) => isEqual(v, item.value))
|
|
68
|
-
:
|
|
88
|
+
: value !== undefined && value !== null && isEqual(value, item.value)
|
|
69
89
|
}
|
|
70
90
|
onClick={() => {
|
|
71
91
|
if (multiSelect) {
|
|
@@ -103,6 +123,7 @@ export const FieldAllowedValues = memo(
|
|
|
103
123
|
disabled,
|
|
104
124
|
name,
|
|
105
125
|
readOnly,
|
|
126
|
+
label,
|
|
106
127
|
}: IFieldAllowedValuesProps) => {
|
|
107
128
|
const fullItems = useMemo(() => {
|
|
108
129
|
const result = [
|
|
@@ -131,6 +152,7 @@ export const FieldAllowedValues = memo(
|
|
|
131
152
|
onChange={onChange}
|
|
132
153
|
value={value}
|
|
133
154
|
name={name}
|
|
155
|
+
label={label}
|
|
134
156
|
/>
|
|
135
157
|
);
|
|
136
158
|
}
|
|
@@ -46,6 +46,58 @@ const numberRenderItem: IArrayAutoFieldProps['renderItem'] = ({
|
|
|
46
46
|
/>
|
|
47
47
|
);
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Hash items are records, so a renderer that casts the item to a string prints
|
|
51
|
+
* `[object Object]` and shows nothing at all. This one reads the record through
|
|
52
|
+
* the schema that describes it: one input per declared field, labelled by that
|
|
53
|
+
* field's `display_name`, carrying the value actually stored under its key.
|
|
54
|
+
*
|
|
55
|
+
* With no `arg_schema` there is nothing to name the parts with, so the record is
|
|
56
|
+
* shown as JSON — unhelpful, but at least it is the data.
|
|
57
|
+
*/
|
|
58
|
+
const hashRenderItem: IArrayAutoFieldProps['renderItem'] = ({
|
|
59
|
+
value,
|
|
60
|
+
onChange,
|
|
61
|
+
disabled,
|
|
62
|
+
readOnly,
|
|
63
|
+
size,
|
|
64
|
+
arg_schema,
|
|
65
|
+
}) => {
|
|
66
|
+
const record = (value ?? {}) as Record<string, unknown>;
|
|
67
|
+
const fields = Object.entries(arg_schema ?? {});
|
|
68
|
+
|
|
69
|
+
if (!fields.length) {
|
|
70
|
+
return (
|
|
71
|
+
<StringFormField
|
|
72
|
+
value={JSON.stringify(record)}
|
|
73
|
+
onChange={() => {}}
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
readOnly
|
|
76
|
+
size={size as any}
|
|
77
|
+
aria-label='Array item input'
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ReqoreControlGroup vertical fluid gapSize='small'>
|
|
84
|
+
{fields.map(([fieldName, fieldSchema]) => (
|
|
85
|
+
<StringFormField
|
|
86
|
+
key={fieldName}
|
|
87
|
+
value={(record[fieldName] as string) ?? ''}
|
|
88
|
+
onChange={(v) => onChange({ ...record, [fieldName]: v })}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
readOnly={readOnly}
|
|
91
|
+
size={size as any}
|
|
92
|
+
aria-label={
|
|
93
|
+
((fieldSchema as { display_name?: string })?.display_name as string) ?? fieldName
|
|
94
|
+
}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</ReqoreControlGroup>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
49
101
|
const meta = {
|
|
50
102
|
component: ArrayAutoField,
|
|
51
103
|
title: 'Components/Form/Auto Array',
|
|
@@ -322,6 +374,7 @@ export const ComplexHashItems: Story = {
|
|
|
322
374
|
type: 'hash',
|
|
323
375
|
value: [{ key: 'value' }],
|
|
324
376
|
display_name: 'Hash Items',
|
|
377
|
+
renderItem: hashRenderItem,
|
|
325
378
|
},
|
|
326
379
|
parameters: {
|
|
327
380
|
docs: {
|
|
@@ -364,6 +417,7 @@ export const HashItemsHeadedByIdentity: Story = {
|
|
|
364
417
|
{ description: 'Added but not yet named' },
|
|
365
418
|
{},
|
|
366
419
|
],
|
|
420
|
+
renderItem: hashRenderItem,
|
|
367
421
|
},
|
|
368
422
|
parameters: {
|
|
369
423
|
docs: {
|
|
@@ -390,5 +444,32 @@ export const HashItemsHeadedByIdentity: Story = {
|
|
|
390
444
|
// And a record with nothing set at all keeps its position — there is
|
|
391
445
|
// genuinely nothing to call it.
|
|
392
446
|
await expect(headings[3]).toContain('#4');
|
|
447
|
+
|
|
448
|
+
// The heading is only half the story: each item must also render the record
|
|
449
|
+
// it is heading, field by field. Asserting the inputs is what would have
|
|
450
|
+
// caught them all printing "[object Object]" — the heading resolved
|
|
451
|
+
// correctly while every value below it was a stringified hash.
|
|
452
|
+
const nameInputs = document.querySelectorAll<HTMLInputElement>(
|
|
453
|
+
'input[aria-label="Method Name"]'
|
|
454
|
+
);
|
|
455
|
+
const descInputs = document.querySelectorAll<HTMLInputElement>(
|
|
456
|
+
'input[aria-label="Description"]'
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
await expect(nameInputs.length).toBe(4);
|
|
460
|
+
await expect(descInputs.length).toBe(4);
|
|
461
|
+
await expect(nameInputs[0].value).toBe('init');
|
|
462
|
+
await expect(descInputs[0].value).toBe('The init method');
|
|
463
|
+
await expect(nameInputs[1].value).toBe('onOrderStatus');
|
|
464
|
+
// The unnamed record: no name to show, but the description it IS headed by
|
|
465
|
+
// has to be there.
|
|
466
|
+
await expect(nameInputs[2].value).toBe('');
|
|
467
|
+
await expect(descInputs[2].value).toBe('Added but not yet named');
|
|
468
|
+
// And the empty one renders empty inputs rather than an empty item.
|
|
469
|
+
await expect(nameInputs[3].value).toBe('');
|
|
470
|
+
await expect(descInputs[3].value).toBe('');
|
|
471
|
+
|
|
472
|
+
// Nothing anywhere may read as a stringified record.
|
|
473
|
+
await expect(document.body.textContent).not.toContain('[object Object]');
|
|
393
474
|
},
|
|
394
475
|
};
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { StoryObj } from '@storybook/react-vite';
|
|
12
12
|
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
13
13
|
import jsyaml from 'js-yaml';
|
|
14
|
-
import { useState } from 'react';
|
|
14
|
+
import { useEffect, useState } from 'react';
|
|
15
15
|
|
|
16
16
|
import { StoryMeta } from '../../../../types';
|
|
17
17
|
import { buildTemplates } from '../../../../helpers/templates';
|
|
@@ -512,3 +512,49 @@ export const ListWithElementType: Story = {
|
|
|
512
512
|
},
|
|
513
513
|
},
|
|
514
514
|
};
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* A named `arg_schema` whose fetch fails.
|
|
518
|
+
*
|
|
519
|
+
* The field asks the server for `dataprovider/arg_schemas/<name>`. When that
|
|
520
|
+
* request rejected — a malformed instance URL, an aborted request, a transport
|
|
521
|
+
* error — the rejection escaped the effect's async IIFE unhandled, `setError`
|
|
522
|
+
* never ran, and `finalArgSchema` stayed undefined. The loading branch then
|
|
523
|
+
* returned on every subsequent render and the error branch below it was
|
|
524
|
+
* unreachable, so the field showed "Loading field data..." for the life of the
|
|
525
|
+
* form: a request that failed instantly and one that is merely slow looked
|
|
526
|
+
* exactly the same, forever.
|
|
527
|
+
*
|
|
528
|
+
* The transport is stubbed rather than left to the dev server, so the story
|
|
529
|
+
* exercises that path and only that path.
|
|
530
|
+
*/
|
|
531
|
+
export const NamedArgSchemaFetchFails: Story = {
|
|
532
|
+
args: {
|
|
533
|
+
name: 'rules',
|
|
534
|
+
type: 'list',
|
|
535
|
+
element_type: 'hash',
|
|
536
|
+
arg_schema: 'a-schema-that-does-not-resolve' as any,
|
|
537
|
+
},
|
|
538
|
+
parameters: {
|
|
539
|
+
docs: {
|
|
540
|
+
description: {
|
|
541
|
+
story:
|
|
542
|
+
'The named arg schema could not be fetched, so the field says so instead of showing an endless "Loading field data..." spinner with the error it had already received never rendered.',
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
decorators: [
|
|
547
|
+
(Story) => {
|
|
548
|
+
const real = window.fetch;
|
|
549
|
+
window.fetch = (() =>
|
|
550
|
+
Promise.reject(new Error('Failed to fetch the field schema'))) as typeof window.fetch;
|
|
551
|
+
useEffect(() => () => {
|
|
552
|
+
window.fetch = real;
|
|
553
|
+
}, []);
|
|
554
|
+
return <Story />;
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
play: async () => {
|
|
558
|
+
await waitForText(/Failed to fetch the field schema|could not be loaded/);
|
|
559
|
+
},
|
|
560
|
+
};
|
|
@@ -18,6 +18,7 @@ import React, { memo, useCallback, useEffect, useState } from 'react';
|
|
|
18
18
|
import { useUpdateEffect } from 'react-use';
|
|
19
19
|
import useMount from 'react-use/lib/useMount';
|
|
20
20
|
import { typedToYaml, yamlToTyped } from '../../../../helpers/common';
|
|
21
|
+
import { getListElementValue } from '../../../../helpers/options';
|
|
21
22
|
import {
|
|
22
23
|
getTypeFromValue,
|
|
23
24
|
getValueOrDefaultValue,
|
|
@@ -212,16 +213,31 @@ function AutoField<T = any>({
|
|
|
212
213
|
useEffect(() => {
|
|
213
214
|
if (typeof arg_schema === 'string') {
|
|
214
215
|
(async () => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
216
|
+
/* The await is guarded because `query` can REJECT rather than answer
|
|
217
|
+
`{ ok: false }` — a malformed instance URL, an aborted request, a
|
|
218
|
+
transport error. Unguarded, that rejection escaped the async IIFE as
|
|
219
|
+
an unhandled promise rejection, `setError` never ran, `finalArgSchema`
|
|
220
|
+
stayed undefined, and the field showed "Loading field data..." for the
|
|
221
|
+
life of the form: a request that failed instantly and a request that
|
|
222
|
+
is merely slow looked exactly the same, forever. */
|
|
223
|
+
try {
|
|
224
|
+
const schema = await query<IOptionsSchema>({
|
|
225
|
+
url: `dataprovider/arg_schemas/${arg_schema}`,
|
|
226
|
+
method: 'GET',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (schema.ok) {
|
|
230
|
+
setError(undefined);
|
|
231
|
+
setFinalArgSchema(schema.data);
|
|
232
|
+
} else {
|
|
233
|
+
setError(schema.error ?? `The schema "${arg_schema}" could not be loaded.`);
|
|
234
|
+
}
|
|
235
|
+
} catch (fetchError) {
|
|
236
|
+
setError(
|
|
237
|
+
fetchError instanceof Error ?
|
|
238
|
+
fetchError.message
|
|
239
|
+
: `The schema "${arg_schema}" could not be loaded.`
|
|
240
|
+
);
|
|
225
241
|
}
|
|
226
242
|
})();
|
|
227
243
|
} else {
|
|
@@ -363,6 +379,18 @@ function AutoField<T = any>({
|
|
|
363
379
|
return (
|
|
364
380
|
<Override
|
|
365
381
|
{...rest}
|
|
382
|
+
// The field's SHAPE, which `rest` does not carry: these three are
|
|
383
|
+
// destructured into named props above, so a host editor received a
|
|
384
|
+
// field it could not describe. `arg_schema` is the sub-schema a host
|
|
385
|
+
// sub-form renders FROM — without it the IDE's test-cases drawer fell
|
|
386
|
+
// back to humanising each key, so `Path`, `Actual` and `Expected` were
|
|
387
|
+
// asked for with no label of their own and no description, while the
|
|
388
|
+
// text sat one fetch away. `element_type`/`ui_element_type` travel with
|
|
389
|
+
// it: a sub-schema alone cannot say whether the value is one hash or a
|
|
390
|
+
// list of them.
|
|
391
|
+
arg_schema={arg_schema}
|
|
392
|
+
element_type={element_type}
|
|
393
|
+
ui_element_type={ui_element_type}
|
|
366
394
|
name={name}
|
|
367
395
|
value={value}
|
|
368
396
|
onChange={(val: any, emittedType?: IQorusType, emittedIsFunction?: boolean) => {
|
|
@@ -407,6 +435,23 @@ function AutoField<T = any>({
|
|
|
407
435
|
);
|
|
408
436
|
}
|
|
409
437
|
|
|
438
|
+
/* The error is checked BEFORE the spinner, and the spinner waits on the error
|
|
439
|
+
being absent.
|
|
440
|
+
|
|
441
|
+
A named `arg_schema` is fetched (`dataprovider/arg_schemas/<name>`), and when
|
|
442
|
+
that fetch failed `finalArgSchema` stayed undefined forever — so this spinner
|
|
443
|
+
returned on every subsequent render and the `if (error)` branch further down
|
|
444
|
+
was unreachable. The field sat on "Loading field data..." for the life of the
|
|
445
|
+
form with no way to find out why, which is indistinguishable from a request
|
|
446
|
+
that is merely slow. */
|
|
447
|
+
if (arg_schema && !finalArgSchema && error) {
|
|
448
|
+
return (
|
|
449
|
+
<ReqoreMessage intent='danger' size='small'>
|
|
450
|
+
{error}
|
|
451
|
+
</ReqoreMessage>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
410
455
|
if (arg_schema && !finalArgSchema) {
|
|
411
456
|
return <ReqoreSpinner size='small'>Loading field data...</ReqoreSpinner>;
|
|
412
457
|
}
|
|
@@ -424,6 +469,9 @@ function AutoField<T = any>({
|
|
|
424
469
|
value={value}
|
|
425
470
|
name={name}
|
|
426
471
|
onChange={handleChange}
|
|
472
|
+
// The picker heads itself with the field's own name rather than a
|
|
473
|
+
// generic "Select one:".
|
|
474
|
+
label={rest.display_name}
|
|
427
475
|
size={rest.size}
|
|
428
476
|
disabled={rest.disabled}
|
|
429
477
|
app={rest.app}
|
|
@@ -507,6 +555,12 @@ function AutoField<T = any>({
|
|
|
507
555
|
return (
|
|
508
556
|
<LongStringFormField
|
|
509
557
|
{...rest}
|
|
558
|
+
// These four share one editor but not one shape: a `string` holds
|
|
559
|
+
// exactly one line, while `data`, `binary` and `long-string` hold
|
|
560
|
+
// a document. Without the type the field cannot tell them apart
|
|
561
|
+
// and treats them all as documents, so an interface's Internal
|
|
562
|
+
// Name — which becomes a YAML key — accepted Enter.
|
|
563
|
+
type={currentType}
|
|
510
564
|
onChange={(value) => handleChange(name, value)}
|
|
511
565
|
value={value}
|
|
512
566
|
/>
|
|
@@ -644,8 +698,16 @@ function AutoField<T = any>({
|
|
|
644
698
|
});
|
|
645
699
|
};
|
|
646
700
|
|
|
701
|
+
// An element may arrive bare or in a `{value, type}` envelope. The
|
|
702
|
+
// envelope is what `formatToServerValue` writes, so a list the user
|
|
703
|
+
// has edited in this session is always wrapped — but a list read
|
|
704
|
+
// back from storage holds exactly what the server contract asks
|
|
705
|
+
// for, which for a list of strings is the strings themselves.
|
|
706
|
+
// Reaching for `.value` on those answered `undefined` for every
|
|
707
|
+
// element: the row rendered as an empty, invalid item, and the
|
|
708
|
+
// emptiness was then written back over the stored value.
|
|
647
709
|
const formatFromServerValue = (value) => {
|
|
648
|
-
return (value || []).map(
|
|
710
|
+
return (value || []).map(getListElementValue);
|
|
649
711
|
};
|
|
650
712
|
|
|
651
713
|
const mappedAllowedValues = rest.element_allowed_values?.map((ev) => ({
|