@qoretechnologies/reqraft 0.10.43-pr.95.gf7f712f → 0.10.43
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 +20 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +279 -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 +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +163 -75
- 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 +117 -88
- 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 +2 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +21 -4
- 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 +57 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +141 -4
- 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/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +44 -26
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +42 -25
- package/dist/components/form/fields/array/ArrayAutoField.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 +35 -6
- 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/optionUiTypes.d.ts +30 -0
- package/dist/helpers/optionUiTypes.d.ts.map +1 -0
- package/dist/helpers/optionUiTypes.js +148 -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 +391 -29
- package/src/components/form/engine/CompactToolbar.tsx +105 -98
- package/src/components/form/engine/FormEngine.stories.tsx +863 -12
- package/src/components/form/engine/FormEngine.tsx +183 -33
- package/src/components/form/engine/_structuredData/SchemaDataView.stories.tsx +123 -0
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +232 -159
- package/src/components/form/engine/_structuredData/structuredData.ts +20 -0
- package/src/components/form/engine/compactRowStyles.ts +38 -1
- package/src/components/form/engine/compactToolbarContext.ts +8 -0
- package/src/components/form/engine/readFirst.ts +166 -5
- 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/ArrayAuto.tsx +23 -4
- package/src/components/form/fields/array/ArrayAutoField.stories.tsx +134 -0
- package/src/components/form/fields/array/ArrayAutoField.tsx +22 -4
- 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 +314 -1
- package/src/components/form/fields/template/TemplateField.tsx +47 -8
- 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/optionUiTypes.ts +153 -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
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
IQorusFormField,
|
|
3
|
+
IQorusFormSchema,
|
|
4
|
+
TQorusFormFieldSchema,
|
|
5
|
+
TQorusType,
|
|
6
|
+
} from '@qoretechnologies/ts-toolkit';
|
|
2
7
|
import { isRendererOnlyUiType } from './rendererTypes';
|
|
3
|
-
import { isUiEncodedValue } from './_structuredData/structuredData';
|
|
8
|
+
import { isEmptyUiEnvelope, isUiEncodedValue } from './_structuredData/structuredData';
|
|
4
9
|
import { renderExpressionToText } from '../expressions/renderExpressionToText';
|
|
5
10
|
import { IExpressionValue } from '../expressions/types';
|
|
6
11
|
import yaml from 'js-yaml';
|
|
@@ -195,6 +200,13 @@ export const shouldAutoCollapseCompactOption = (
|
|
|
195
200
|
* Prefer the matching allowed_values entry's display_name (fallback `name`)
|
|
196
201
|
* over the raw stored value.
|
|
197
202
|
*
|
|
203
|
+
* A LIST carries its options under `element_allowed_values` — they constrain
|
|
204
|
+
* each element, not the list itself — so a multi-select that was missing here
|
|
205
|
+
* printed what it stores rather than what you picked: `orders, batch` for
|
|
206
|
+
* fields whose picker reads "Orders, Batch". The gap shows worst exactly where
|
|
207
|
+
* allowed values earn their keep, since the stored form is often not readable
|
|
208
|
+
* at all (a permission code, `PO_REQUIRE_TYPES`, an app-specific id).
|
|
209
|
+
*
|
|
198
210
|
* Exported (as `findAllowedValueOption`) because the form engine's value
|
|
199
211
|
* validation has to answer the SAME question — "is this stored value one of the
|
|
200
212
|
* declared choices?" — and answering it differently loses data. An
|
|
@@ -210,13 +222,30 @@ export const findAllowedValueOption = (
|
|
|
210
222
|
value: unknown,
|
|
211
223
|
schema?: TQorusFormFieldSchema
|
|
212
224
|
): any | undefined => {
|
|
213
|
-
const s = schema as
|
|
214
|
-
|
|
225
|
+
const s = schema as
|
|
226
|
+
{ allowed_values?: any[]; element_allowed_values?: any[]; items?: any[] } | undefined;
|
|
227
|
+
const options =
|
|
228
|
+
(s?.allowed_values?.length && s.allowed_values) ||
|
|
229
|
+
(s?.element_allowed_values?.length && s.element_allowed_values) ||
|
|
230
|
+
s?.items;
|
|
215
231
|
if (!options?.length) {
|
|
216
232
|
return undefined;
|
|
217
233
|
}
|
|
234
|
+
// A stored element can be the bare value or the typed `{type, value}`
|
|
235
|
+
// envelope the form engine round-trips; match either against either.
|
|
236
|
+
const stored =
|
|
237
|
+
value && typeof value === 'object' && 'value' in (value as Record<string, unknown>) ?
|
|
238
|
+
(value as Record<string, unknown>).value
|
|
239
|
+
: value;
|
|
240
|
+
|
|
218
241
|
return options.find(
|
|
219
|
-
(option) =>
|
|
242
|
+
(option) =>
|
|
243
|
+
option?.value?.value === value ||
|
|
244
|
+
option?.value === value ||
|
|
245
|
+
option?.name === value ||
|
|
246
|
+
option?.value?.value === stored ||
|
|
247
|
+
option?.value === stored ||
|
|
248
|
+
option?.name === stored
|
|
220
249
|
);
|
|
221
250
|
};
|
|
222
251
|
|
|
@@ -646,12 +675,24 @@ export const getHashEntries = (
|
|
|
646
675
|
// whose entries are `{ type: 'string', value: 'x' }`) — recognise those by
|
|
647
676
|
// their strict shape (a string `type`, a `value`, and no foreign keys) so
|
|
648
677
|
// the sub-row shows 'x' rather than counting the envelope's own keys.
|
|
678
|
+
// `'value' in raw` is FALSE for a materialised-but-unset field: `fixOptions`
|
|
679
|
+
// omits the key entirely when the value is undefined, leaving `{type: 'x'}`.
|
|
680
|
+
// Requiring `value` therefore classed that envelope as a raw hash VALUE and
|
|
681
|
+
// counted its own `type` key, so an untouched Runtime Defaults collapsed to
|
|
682
|
+
// "Timeout (Seconds) 1 field" three times over while the sub-form correctly
|
|
683
|
+
// said 0/3 set. With a sub-schema present, a `type`-only object is an empty
|
|
684
|
+
// envelope, never content.
|
|
685
|
+
// Shared with SchemaDataView, which previews this same value one level
|
|
686
|
+
// down: two renderers disagreeing about what an unset field looks like is
|
|
687
|
+
// how the row said "1 field" long after this one stopped.
|
|
688
|
+
const isEmptyEnvelope = !!subSchema && isEmptyUiEnvelope(raw);
|
|
649
689
|
const isFieldShape =
|
|
650
690
|
(!!subSchema &&
|
|
651
691
|
!!raw &&
|
|
652
692
|
typeof raw === 'object' &&
|
|
653
693
|
!Array.isArray(raw) &&
|
|
654
694
|
'value' in (raw as object)) ||
|
|
695
|
+
isEmptyEnvelope ||
|
|
655
696
|
isTypedEnvelope(raw);
|
|
656
697
|
const subOption: IQorusFormField =
|
|
657
698
|
isFieldShape ?
|
|
@@ -793,3 +834,123 @@ export const getFirstAttentionOptionName = (
|
|
|
793
834
|
|
|
794
835
|
return undefined;
|
|
795
836
|
};
|
|
837
|
+
|
|
838
|
+
/* ------------------------------------------------------------------------- */
|
|
839
|
+
/* Record identity — which field says WHICH item this is */
|
|
840
|
+
/* */
|
|
841
|
+
/* Lives here, beside the formatters it uses, rather than in the view that */
|
|
842
|
+
/* first needed it: it is data logic, not rendering, and BOTH list renderers */
|
|
843
|
+
/* plus their tests import it. Keeping it in `SchemaDataView` dragged React, */
|
|
844
|
+
/* styled-components and every Reqore component it renders into any module */
|
|
845
|
+
/* that only wanted to know an item's name — which broke `arrayAutoField`'s */
|
|
846
|
+
/* mocked-Reqore test the moment the editable list started using it. */
|
|
847
|
+
/* ------------------------------------------------------------------------- */
|
|
848
|
+
|
|
849
|
+
/** A UI-encoded value carries its type alongside it; the record's own value is inside. */
|
|
850
|
+
const unwrap = (value: unknown): unknown =>
|
|
851
|
+
isUiEncodedValue(value) ? (value as { value: unknown }).value : value;
|
|
852
|
+
|
|
853
|
+
/** Whether a value is worth a row of its own. `false` and `0` are values. */
|
|
854
|
+
export const isSet = (value: unknown): boolean =>
|
|
855
|
+
value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && !value.length);
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* The keys to render, in the order to render them: the schema's own order first
|
|
859
|
+
* (that is the order the form puts the fields in), then anything stored that the
|
|
860
|
+
* schema does not mention, so undescribed data is shown rather than dropped.
|
|
861
|
+
*/
|
|
862
|
+
export const orderedKeys = (
|
|
863
|
+
record: Record<string, unknown>,
|
|
864
|
+
schema: IQorusFormSchema
|
|
865
|
+
): string[] => {
|
|
866
|
+
const described = Object.keys(schema).filter((key) => isSet(unwrap(record[key])));
|
|
867
|
+
const extra = Object.keys(record).filter((key) => !(key in schema) && isSet(unwrap(record[key])));
|
|
868
|
+
return [...described, ...extra];
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
/** A field whose content is source code. `ui_type` is what the form renders by,
|
|
872
|
+
* so it is what the preview reads by too — the storage type is just `string`. */
|
|
873
|
+
export const isCodeField = (fieldSchema: TQorusFormFieldSchema | undefined): boolean => {
|
|
874
|
+
const uiType = (fieldSchema as { ui_type?: string } | undefined)?.ui_type;
|
|
875
|
+
return uiType === 'code-editor';
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The field whose value heads the item — the first DECLARED one holding a plain
|
|
880
|
+
* scalar.
|
|
881
|
+
*
|
|
882
|
+
* Schema order, not value order: the first declared field is the one the form
|
|
883
|
+
* puts at the top of an item, which is the one that says which item it is. A
|
|
884
|
+
* code body or a nested level is skipped, not because it is unimportant but
|
|
885
|
+
* because it has no one-line form — it belongs in the rows below where it can
|
|
886
|
+
* actually be read.
|
|
887
|
+
*
|
|
888
|
+
* One definition, used both to RENDER the heading and to omit that field from
|
|
889
|
+
* the rows. Computing it twice is how the heading and the rows start disagreeing
|
|
890
|
+
* about which field was promoted, and the item shows its name twice or not at
|
|
891
|
+
* all.
|
|
892
|
+
*/
|
|
893
|
+
export const titleKeyFor = (
|
|
894
|
+
record: Record<string, unknown>,
|
|
895
|
+
schema: IQorusFormSchema
|
|
896
|
+
): string | undefined =>
|
|
897
|
+
orderedKeys(record, schema).find((key) => {
|
|
898
|
+
const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
|
|
899
|
+
if (isCodeField(fieldSchema)) {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
const raw = unwrap(record[key]);
|
|
903
|
+
return typeof raw === 'string' || typeof raw === 'number';
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
/** What identifies one record: which field was promoted, and how to draw it. */
|
|
907
|
+
export interface IRecordIdentity {
|
|
908
|
+
/** The promoted field's key, so a caller can omit it from the rows below. */
|
|
909
|
+
key: string;
|
|
910
|
+
/** The value, formatted the way its own row would format it. */
|
|
911
|
+
text: string;
|
|
912
|
+
/** The promoted field's label — a caption for the heading, not a second line. */
|
|
913
|
+
label: string;
|
|
914
|
+
/** A literal keeps its mono face; a chosen label is prose. */
|
|
915
|
+
mono: boolean;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* The identity of one record, for any surface that heads a list item with it.
|
|
920
|
+
*
|
|
921
|
+
* Exported because the preview is not the only place a list of records is shown:
|
|
922
|
+
* the editable list (`ArrayAuto`) heads the same records, and it must promote the
|
|
923
|
+
* same field and format it the same way. Two implementations would drift the
|
|
924
|
+
* moment one of them learned about a new field type, and the reader would meet
|
|
925
|
+
* an item called `init` in the preview and `#1` in the editor.
|
|
926
|
+
*/
|
|
927
|
+
export const recordIdentity = (
|
|
928
|
+
record: Record<string, unknown>,
|
|
929
|
+
schema: IQorusFormSchema | undefined
|
|
930
|
+
): IRecordIdentity | undefined => {
|
|
931
|
+
if (!schema) {
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const key = titleKeyFor(record, schema);
|
|
936
|
+
|
|
937
|
+
if (!key) {
|
|
938
|
+
return undefined;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const fieldSchema = schema[key] as TQorusFormFieldSchema | undefined;
|
|
942
|
+
const raw = unwrap(record[key]);
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
key,
|
|
946
|
+
text: formatOptionValue(
|
|
947
|
+
{ type: fieldSchema?.type, value: raw } as IQorusFormField,
|
|
948
|
+
fieldSchema
|
|
949
|
+
),
|
|
950
|
+
label: fieldLabel(key, fieldSchema),
|
|
951
|
+
mono: !findAllowedValueOption(raw, fieldSchema),
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
export const fieldLabel = (key: string, fieldSchema: TQorusFormFieldSchema | undefined): string =>
|
|
956
|
+
(fieldSchema as { display_name?: string } | undefined)?.display_name || key;
|
|
@@ -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
|
}
|
|
@@ -24,6 +24,7 @@ import { validateFieldWithResult } from '../../../../helpers/validations';
|
|
|
24
24
|
import { useWhyDidYouUpdate } from '../../../../hooks/useWhyDidYouUpdate';
|
|
25
25
|
// Direct imports — the cycle (ArrayAuto → TemplateField/auto → ArrayAuto) is
|
|
26
26
|
// safe the same way Field → AutoFormField → Field is.
|
|
27
|
+
import { recordIdentity } from '../../engine/readFirst';
|
|
27
28
|
import { AutoFormField } from '../auto/AutoFormField';
|
|
28
29
|
import { TemplateField } from '../template/TemplateField';
|
|
29
30
|
|
|
@@ -133,12 +134,29 @@ export const ArrayAuto = ({
|
|
|
133
134
|
// Render list of auto fields
|
|
134
135
|
return (
|
|
135
136
|
<ReqoreControlGroup vertical fluid>
|
|
136
|
-
{map(localValue, (val: string | number, idx: string) =>
|
|
137
|
+
{map(localValue, (val: string | number, idx: string) => {
|
|
138
|
+
// The same field the collapsed preview promotes, resolved by the same
|
|
139
|
+
// definition — see `recordIdentity`. A list of seven methods headed
|
|
140
|
+
// `#1 … #7` makes the reader open each one to find out which is which,
|
|
141
|
+
// and heading them differently in the preview and the editor would make
|
|
142
|
+
// one item answer to two names.
|
|
143
|
+
const identity =
|
|
144
|
+
val && typeof val === 'object' && !Array.isArray(val)
|
|
145
|
+
? recordIdentity(val as Record<string, unknown>, rest.arg_schema)
|
|
146
|
+
: undefined;
|
|
147
|
+
|
|
148
|
+
return (
|
|
137
149
|
<ReqorePanel
|
|
138
150
|
key={idx}
|
|
139
|
-
|
|
151
|
+
// `#N` stays as the fallback: a row whose identifying field is still
|
|
152
|
+
// empty (one just added) has nothing to be called yet, and a blank
|
|
153
|
+
// heading would be worse than a number.
|
|
154
|
+
label={identity?.text || `#${idx + 1}`}
|
|
155
|
+
tooltip={identity?.label}
|
|
140
156
|
unMountContentOnCollapse={false}
|
|
141
|
-
badge
|
|
157
|
+
// The position moves to the badge once the name owns the heading, so
|
|
158
|
+
// "which of these is third" stays answerable.
|
|
159
|
+
badge={identity ? `#${idx + 1}` : display_name || name}
|
|
142
160
|
collapsible
|
|
143
161
|
responsiveActions={false}
|
|
144
162
|
responsiveTitle={false}
|
|
@@ -188,7 +206,8 @@ export const ArrayAuto = ({
|
|
|
188
206
|
/>
|
|
189
207
|
{showValidationMessage(val)}
|
|
190
208
|
</ReqorePanel>
|
|
191
|
-
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
192
211
|
<ReqoreControlGroup fluid>
|
|
193
212
|
<ReqoreButton
|
|
194
213
|
onClick={addValue}
|
|
@@ -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: {
|
|
@@ -339,3 +392,84 @@ export const ComplexHashItems: Story = {
|
|
|
339
392
|
await expect(document.querySelectorAll('.array-auto-compact-tag').length).toBe(0);
|
|
340
393
|
},
|
|
341
394
|
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The editable list, headed by what identifies each record.
|
|
398
|
+
*
|
|
399
|
+
* The rows used to read `#1 #2 #3`, so finding one method meant opening each in
|
|
400
|
+
* turn — and the collapsed preview beside it already headed the same records
|
|
401
|
+
* `init` / `onOrderStatus`, which made one item answer to two names. Both now
|
|
402
|
+
* resolve the heading through `recordIdentity`: the first field the SCHEMA
|
|
403
|
+
* declares that holds a scalar, skipping any it leaves unset. The position keeps
|
|
404
|
+
* a badge, so "the third one" is still answerable.
|
|
405
|
+
*/
|
|
406
|
+
export const HashItemsHeadedByIdentity: Story = {
|
|
407
|
+
args: {
|
|
408
|
+
type: 'hash',
|
|
409
|
+
display_name: 'Service Methods',
|
|
410
|
+
arg_schema: {
|
|
411
|
+
name: { type: 'string', ui_type: 'string', display_name: 'Method Name', required: true },
|
|
412
|
+
description: { type: 'string', ui_type: 'string', display_name: 'Description' },
|
|
413
|
+
},
|
|
414
|
+
value: [
|
|
415
|
+
{ name: 'init', description: 'The init method' },
|
|
416
|
+
{ name: 'onOrderStatus', description: 'Handles order status callbacks' },
|
|
417
|
+
{ description: 'Added but not yet named' },
|
|
418
|
+
{},
|
|
419
|
+
],
|
|
420
|
+
renderItem: hashRenderItem,
|
|
421
|
+
},
|
|
422
|
+
parameters: {
|
|
423
|
+
docs: {
|
|
424
|
+
description: {
|
|
425
|
+
story:
|
|
426
|
+
'Renders four hash items headed by their identifying value: two named methods read as "init" and "onOrderStatus", one with no name falls through to its description, and an entirely empty one keeps its position number.',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
async play() {
|
|
431
|
+
await waitFor(() => expect(document.querySelectorAll('.array-auto-item').length).toBe(4), {
|
|
432
|
+
timeout: 5000,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const headings = [...document.querySelectorAll('.array-auto-item')].map(
|
|
436
|
+
(item) => item.textContent ?? ''
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
await expect(headings[0]).toContain('init');
|
|
440
|
+
await expect(headings[1]).toContain('onOrderStatus');
|
|
441
|
+
// An unset field is skipped rather than promoted blank, so this one is
|
|
442
|
+
// headed by the next field that IS set.
|
|
443
|
+
await expect(headings[2]).toContain('Added but not yet named');
|
|
444
|
+
// And a record with nothing set at all keeps its position — there is
|
|
445
|
+
// genuinely nothing to call it.
|
|
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]');
|
|
474
|
+
},
|
|
475
|
+
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@qoretechnologies/reqore';
|
|
10
10
|
import { IQorusFormSchema } from '@qoretechnologies/ts-toolkit';
|
|
11
11
|
import { map, size } from 'lodash';
|
|
12
|
+
import { recordIdentity } from '../../engine/readFirst';
|
|
12
13
|
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
|
13
14
|
import { useDebounce } from 'react-use';
|
|
14
15
|
import { validateFieldWithResult } from '../../../../helpers/validations';
|
|
@@ -279,11 +280,27 @@ export const ArrayAutoField = memo(
|
|
|
279
280
|
|
|
280
281
|
return (
|
|
281
282
|
<ReqoreControlGroup vertical fluid>
|
|
282
|
-
{map(localValue, (val, idx) =>
|
|
283
|
+
{map(localValue, (val, idx) => {
|
|
284
|
+
// The same field the collapsed preview promotes, resolved by the same
|
|
285
|
+
// definition — see `recordIdentity`. Both editable list renderers head
|
|
286
|
+
// their records with it, so an item cannot be called `init` in one
|
|
287
|
+
// place and `#1` in another.
|
|
288
|
+
const identity =
|
|
289
|
+
val && typeof val === 'object' && !Array.isArray(val)
|
|
290
|
+
? recordIdentity(val as Record<string, unknown>, arg_schema)
|
|
291
|
+
: undefined;
|
|
292
|
+
|
|
293
|
+
return (
|
|
283
294
|
<ReqorePanel
|
|
284
295
|
key={idx}
|
|
285
|
-
|
|
286
|
-
|
|
296
|
+
// `#N` stays as the fallback: a record whose identifying field is
|
|
297
|
+
// still empty has nothing to be called yet, and a blank heading
|
|
298
|
+
// would be worse than a number.
|
|
299
|
+
label={identity?.text || `#${idx + 1}`}
|
|
300
|
+
tooltip={identity?.label}
|
|
301
|
+
// The position moves to the badge once the name owns the heading,
|
|
302
|
+
// so "which of these is third" stays answerable.
|
|
303
|
+
badge={identity ? `#${idx + 1}` : display_name || name}
|
|
287
304
|
collapsible
|
|
288
305
|
unMountContentOnCollapse={false}
|
|
289
306
|
responsiveActions={false}
|
|
@@ -321,7 +338,8 @@ export const ArrayAutoField = memo(
|
|
|
321
338
|
})}
|
|
322
339
|
{renderValidationMessage(val)}
|
|
323
340
|
</ReqorePanel>
|
|
324
|
-
|
|
341
|
+
);
|
|
342
|
+
})}
|
|
325
343
|
|
|
326
344
|
<ReqoreButton
|
|
327
345
|
onClick={addItem}
|
|
@@ -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
|
+
};
|