@qoretechnologies/reqraft 0.10.15 → 0.10.17
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/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +82 -3
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +57 -11
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +199 -110
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +8 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +1 -1
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/optionActions.d.ts +28 -0
- package/dist/components/form/engine/optionActions.d.ts.map +1 -0
- package/dist/components/form/engine/optionActions.js +18 -0
- package/dist/components/form/engine/optionActions.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +9 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +15 -3
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +35 -0
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -0
- package/dist/components/form/engine/rendererTypes.js +63 -0
- package/dist/components/form/engine/rendererTypes.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +6 -1
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +2 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/hooks/useMediaQuery.d.ts +24 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +56 -0
- package/dist/hooks/useMediaQuery.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/form/engine/CompactRow.tsx +139 -0
- package/src/components/form/engine/FormEngine.stories.tsx +73 -0
- package/src/components/form/engine/FormEngine.tsx +270 -70
- package/src/components/form/engine/FormEngineRemote.stories.tsx +329 -0
- package/src/components/form/engine/compactRowContext.ts +8 -0
- package/src/components/form/engine/compactRowStyles.ts +30 -0
- package/src/components/form/engine/optionActions.ts +40 -0
- package/src/components/form/engine/readFirst.ts +25 -6
- package/src/components/form/engine/rendererTypes.ts +55 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +6 -1
- package/src/components/form/index.tsx +2 -0
- package/src/hooks/useMediaQuery.ts +61 -0
- package/src/index.tsx +7 -0
|
@@ -15,10 +15,7 @@ import {
|
|
|
15
15
|
} from '@qoretechnologies/reqore';
|
|
16
16
|
import { IReqoreCollectionProps } from '@qoretechnologies/reqore/dist/components/Collection';
|
|
17
17
|
import { IReqoreCollectionItemProps } from '@qoretechnologies/reqore/dist/components/Collection/item';
|
|
18
|
-
import {
|
|
19
|
-
IReqorePanelAction,
|
|
20
|
-
IReqorePanelProps,
|
|
21
|
-
} from '@qoretechnologies/reqore/dist/components/Panel';
|
|
18
|
+
import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel';
|
|
22
19
|
import { IReqoreFormTemplates } from '@qoretechnologies/reqore/dist/components/Textarea';
|
|
23
20
|
import { TReqoreIntent } from '@qoretechnologies/reqore/dist/constants/theme';
|
|
24
21
|
import {
|
|
@@ -41,6 +38,9 @@ import {
|
|
|
41
38
|
TQorusFormOperatorValue,
|
|
42
39
|
TQorusType,
|
|
43
40
|
} from '@qoretechnologies/ts-toolkit';
|
|
41
|
+
import { resolveOptionActions, TOptionActions } from './optionActions';
|
|
42
|
+
import { createRendererOnlyUiTypeCheck, isRendererOnlyUiType } from './rendererTypes';
|
|
43
|
+
import { useCanHover, useIsNarrowViewport } from '../../../hooks/useMediaQuery';
|
|
44
44
|
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last } from 'lodash';
|
|
45
45
|
import isArray from 'lodash/isArray';
|
|
46
46
|
import map from 'lodash/map';
|
|
@@ -245,12 +245,16 @@ const StyledCommitDock = styled.div<{ $bg: string; $border: string }>`
|
|
|
245
245
|
`;
|
|
246
246
|
|
|
247
247
|
export const getType = (
|
|
248
|
-
type
|
|
248
|
+
type?: TQorusType | TQorusType[],
|
|
249
249
|
operators?: IOperatorsSchema,
|
|
250
250
|
operator?: TOperatorValue
|
|
251
251
|
): TQorusType => {
|
|
252
252
|
const finalType = getTypeFromOperator(operators, fixOperatorValue(operator)) || type;
|
|
253
|
-
|
|
253
|
+
const resolvedType = isArray(finalType) ? finalType[0] : finalType;
|
|
254
|
+
|
|
255
|
+
// A value can be received before its server-driven option schema. Keep the
|
|
256
|
+
// renderer type-safe during that transition without guessing a concrete type.
|
|
257
|
+
return (resolvedType || 'any') as TQorusType;
|
|
254
258
|
};
|
|
255
259
|
|
|
256
260
|
const getTypeFromOperator = (
|
|
@@ -267,6 +271,38 @@ export const fixOperatorValue = (operator: TOperatorValue): (string | null | und
|
|
|
267
271
|
return isArray(operator) ? operator : [operator];
|
|
268
272
|
};
|
|
269
273
|
|
|
274
|
+
/**
|
|
275
|
+
* The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
|
|
276
|
+
* instance passes its own (built-ins + the `rendererOnlyUiTypes` prop) so a
|
|
277
|
+
* consumer's injected editors are recognised too. See `./rendererTypes`.
|
|
278
|
+
*/
|
|
279
|
+
type TRendererOnlyCheck = (type?: TQorusType | TQorusType[]) => boolean;
|
|
280
|
+
|
|
281
|
+
const getOptionSchemaStorageType = (
|
|
282
|
+
option?: TQorusFormFieldSchema,
|
|
283
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
284
|
+
): TQorusType =>
|
|
285
|
+
((isRendererOnly(option?.ui_type)
|
|
286
|
+
? option?.type || option?.ui_type
|
|
287
|
+
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
288
|
+
|
|
289
|
+
const getOptionFieldStorageType = (
|
|
290
|
+
optionName: string,
|
|
291
|
+
fieldType: TQorusType | TQorusType[] | undefined,
|
|
292
|
+
schema?: IQorusFormSchema,
|
|
293
|
+
operators?: IOperatorsSchema,
|
|
294
|
+
operatorData?: TOperatorValue,
|
|
295
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
296
|
+
): TQorusType => {
|
|
297
|
+
const schemaOption = schema?.[optionName];
|
|
298
|
+
const storedType =
|
|
299
|
+
fieldType && !(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
|
|
300
|
+
? fieldType
|
|
301
|
+
: getOptionSchemaStorageType(schemaOption, isRendererOnly);
|
|
302
|
+
|
|
303
|
+
return getType(storedType as TQorusType, operators, operatorData);
|
|
304
|
+
};
|
|
305
|
+
|
|
270
306
|
export const hasRequiredOptions = (options: IQorusFormSchema = {}) => {
|
|
271
307
|
return !!findKey(options, (option) => option.required);
|
|
272
308
|
};
|
|
@@ -278,11 +314,19 @@ export const OptionsContext = createContext<{
|
|
|
278
314
|
|
|
279
315
|
export const fixOptions = (
|
|
280
316
|
value: TQorusForm | TQorusFlatForm = {},
|
|
281
|
-
options
|
|
282
|
-
operators?: IOperatorsSchema
|
|
317
|
+
options?: IQorusFormSchema,
|
|
318
|
+
operators?: IOperatorsSchema,
|
|
319
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
283
320
|
): TQorusForm => {
|
|
284
321
|
const fixedValue = cloneDeep(value);
|
|
285
322
|
|
|
323
|
+
// Server-driven schemas can arrive after the persisted value. Preserve that
|
|
324
|
+
// value verbatim until its schema is available instead of manufacturing
|
|
325
|
+
// partially typed form fields that can crash the following render.
|
|
326
|
+
if (!size(options)) {
|
|
327
|
+
return fixedValue as TQorusForm;
|
|
328
|
+
}
|
|
329
|
+
|
|
286
330
|
forEach(options, (option, name) => {
|
|
287
331
|
if (
|
|
288
332
|
option.value ||
|
|
@@ -292,7 +336,7 @@ export const fixOptions = (
|
|
|
292
336
|
) {
|
|
293
337
|
let obj: IQorusFormField;
|
|
294
338
|
const type = getType(
|
|
295
|
-
(option
|
|
339
|
+
getOptionSchemaStorageType(option, isRendererOnly),
|
|
296
340
|
operators,
|
|
297
341
|
(fixedValue[name] as IQorusFormField)?.op
|
|
298
342
|
);
|
|
@@ -328,20 +372,35 @@ export const fixOptions = (
|
|
|
328
372
|
let newOption = option as IQorusFormField;
|
|
329
373
|
|
|
330
374
|
if (!isPlainObject(newOption) || !(newOption as IQorusFormField)?.type) {
|
|
375
|
+
const isUntypedEnvelope = isPlainObject(newOption) && 'value' in newOption;
|
|
376
|
+
const untypedFieldValue =
|
|
377
|
+
isUntypedEnvelope ? (newOption as IQorusFormField).value : newOption;
|
|
378
|
+
// Spread the original envelope first so every key it carried survives —
|
|
379
|
+
// rebuilding from just {type, value} silently dropped `op`, `is_expression`
|
|
380
|
+
// and anything else a field needs to render, which emptied the card.
|
|
331
381
|
const fixedOption: IQorusFormField = {
|
|
382
|
+
...(isPlainObject(newOption) ? (newOption as IQorusFormField) : {}),
|
|
332
383
|
type: getType(
|
|
333
|
-
(options?.[optionName]
|
|
384
|
+
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
334
385
|
operators,
|
|
335
386
|
(newOption as IQorusFormField)?.op
|
|
336
387
|
),
|
|
337
|
-
value:
|
|
388
|
+
value: untypedFieldValue,
|
|
338
389
|
};
|
|
339
390
|
|
|
340
|
-
if ((newOption as IQorusFormField)?.is_expression) {
|
|
341
|
-
fixedOption.is_expression = true;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
391
|
newOption = fixedOption;
|
|
392
|
+
} else if (
|
|
393
|
+
newOption.type === options?.[optionName]?.ui_type &&
|
|
394
|
+
isRendererOnly(newOption.type)
|
|
395
|
+
) {
|
|
396
|
+
newOption = {
|
|
397
|
+
...newOption,
|
|
398
|
+
type: getType(
|
|
399
|
+
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
400
|
+
operators,
|
|
401
|
+
newOption.op
|
|
402
|
+
),
|
|
403
|
+
};
|
|
345
404
|
}
|
|
346
405
|
|
|
347
406
|
if (
|
|
@@ -411,7 +470,7 @@ export const flattenOptions = (options: TQorusForm): TQorusFlatForm => {
|
|
|
411
470
|
};
|
|
412
471
|
|
|
413
472
|
export const getTypeAndCanBeNull = (
|
|
414
|
-
type
|
|
473
|
+
type?: TQorusType | TQorusType[],
|
|
415
474
|
allowed_values?: any[],
|
|
416
475
|
operatorData?: TOperatorValue,
|
|
417
476
|
operators?: IOperatorsSchema
|
|
@@ -546,19 +605,36 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
546
605
|
* option's name/schema/value (the context the IDE's `AiAssistanceAction`
|
|
547
606
|
* captures); the consumer injects the button, reqraft stays AI-free.
|
|
548
607
|
*/
|
|
549
|
-
optionActions?:
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
608
|
+
optionActions?: TOptionActions;
|
|
609
|
+
/**
|
|
610
|
+
* Whether per-option injected actions collapse into the row's overflow menu
|
|
611
|
+
* instead of rendering as inline buttons.
|
|
612
|
+
*
|
|
613
|
+
* `'auto'` (default) collapses when the device cannot hover or the viewport is
|
|
614
|
+
* narrow — on a phone a hover-gated action would otherwise be unreachable, and
|
|
615
|
+
* a row has no width for a button strip. `'always'` / `'never'` force it, which
|
|
616
|
+
* consumers can use for a known-mobile surface and stories use to capture the
|
|
617
|
+
* collapsed state deterministically.
|
|
618
|
+
*
|
|
619
|
+
* Actions beyond `MAX_INLINE_OPTION_ACTIONS` always overflow into the menu, so
|
|
620
|
+
* a consumer injecting many actions can never blow out the row.
|
|
621
|
+
*/
|
|
622
|
+
optionActionsCollapse?: 'auto' | 'always' | 'never';
|
|
556
623
|
/**
|
|
557
624
|
* SEAM (reqraft): consumer-injected field editors for types reqraft doesn't
|
|
558
625
|
* ship (IDE domain fields). Keyed by field `type`/`ui_type`; forwarded through
|
|
559
626
|
* `TemplateField` to the `AutoFormField` override seam.
|
|
560
627
|
*/
|
|
561
628
|
componentOverrides?: Record<string, React.FC<any>>;
|
|
629
|
+
/**
|
|
630
|
+
* The `ui_type` names among `componentOverrides` that select a bespoke editor
|
|
631
|
+
* but store their value as the schema's plainer `type` (a `cron` editor stores
|
|
632
|
+
* a string). Declaring them here keeps the field's stored `type` correct —
|
|
633
|
+
* without it the renderer name is written into the value and validation and
|
|
634
|
+
* round-tripping both break. Merged with reqraft's own built-in list, so a
|
|
635
|
+
* consumer's new editor no longer needs a reqraft release to be handled.
|
|
636
|
+
*/
|
|
637
|
+
rendererOnlyUiTypes?: string[];
|
|
562
638
|
|
|
563
639
|
/**
|
|
564
640
|
* Bag of values forwarded from an outer FormEngine scope, used as a
|
|
@@ -586,6 +662,27 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
586
662
|
* classic (non-compact) mode. Default: off.
|
|
587
663
|
*/
|
|
588
664
|
autoFocusFirstRequired?: boolean;
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Names of read-first rows to open on mount, in addition to whatever the
|
|
668
|
+
* user opens afterwards.
|
|
669
|
+
*
|
|
670
|
+
* Which rows are expanded is otherwise private to the engine, which is
|
|
671
|
+
* fine while expansion is only ever a click. It stops being fine when the
|
|
672
|
+
* expanded row is part of an address: a consumer whose field renders its
|
|
673
|
+
* own routable surface (a table whose rows open panes with their own URLs)
|
|
674
|
+
* can restore the pane from the URL, but not the row that has to be open
|
|
675
|
+
* for the pane to exist at all — so a pasted or reloaded link lands on a
|
|
676
|
+
* collapsed form.
|
|
677
|
+
*
|
|
678
|
+
* Applied once, on the first render where the schema has rows, so an
|
|
679
|
+
* async-loaded schema is covered. It never re-expands a row the user has
|
|
680
|
+
* since collapsed, and it does not participate in `expandMode: 'single'`
|
|
681
|
+
* accordion collapsing — the caller is naming a starting point, not
|
|
682
|
+
* driving the state. Only a remount re-arms it. No-op in classic
|
|
683
|
+
* (non-compact) mode.
|
|
684
|
+
*/
|
|
685
|
+
initialExpandedOptions?: string[];
|
|
589
686
|
}
|
|
590
687
|
|
|
591
688
|
// Option types rendered full-width (IDE Options parity, commit 8e6b7781).
|
|
@@ -623,11 +720,27 @@ export const FormEngine = ({
|
|
|
623
720
|
optionsLoader,
|
|
624
721
|
onValidityChange,
|
|
625
722
|
optionActions,
|
|
723
|
+
optionActionsCollapse = 'auto',
|
|
626
724
|
componentOverrides,
|
|
725
|
+
rendererOnlyUiTypes,
|
|
627
726
|
inheritedFromParent,
|
|
628
727
|
autoFocusFirstRequired,
|
|
728
|
+
initialExpandedOptions,
|
|
629
729
|
...rest
|
|
630
730
|
}: IFormEngineProps) => {
|
|
731
|
+
// Built-ins + whatever the consumer declared for its own injected editors.
|
|
732
|
+
const isRendererOnly = useMemo(
|
|
733
|
+
() => createRendererOnlyUiTypeCheck(rendererOnlyUiTypes),
|
|
734
|
+
[JSON.stringify(rendererOnlyUiTypes)]
|
|
735
|
+
);
|
|
736
|
+
// Resolved once here rather than per row — every CompactRow reads the answer
|
|
737
|
+
// off the context instead of opening its own matchMedia subscription.
|
|
738
|
+
const canHover = useCanHover();
|
|
739
|
+
const isNarrowViewport = useIsNarrowViewport();
|
|
740
|
+
const collapseOptionActions =
|
|
741
|
+
optionActionsCollapse === 'always' ? true
|
|
742
|
+
: optionActionsCollapse === 'never' ? false
|
|
743
|
+
: !canHover || isNarrowViewport;
|
|
631
744
|
const [options, setOptions] = useState<IQorusFormSchema | undefined>(rest?.options || undefined);
|
|
632
745
|
// optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
|
|
633
746
|
const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
|
|
@@ -763,7 +876,7 @@ export const FormEngine = ({
|
|
|
763
876
|
fields: TQorusForm | TQorusFlatForm;
|
|
764
877
|
meta?: IOptionsOnChangeMeta;
|
|
765
878
|
}>(() => ({
|
|
766
|
-
fields: fixOptions(value, options || {}),
|
|
879
|
+
fields: fixOptions(value, options || {}, undefined, isRendererOnly),
|
|
767
880
|
meta: undefined,
|
|
768
881
|
}));
|
|
769
882
|
const originalValue = useRef<any>();
|
|
@@ -856,7 +969,7 @@ export const FormEngine = ({
|
|
|
856
969
|
setOptions({});
|
|
857
970
|
return;
|
|
858
971
|
}
|
|
859
|
-
setLocalValue({ fields: fixOptions(value, data.data), meta: undefined });
|
|
972
|
+
setLocalValue({ fields: fixOptions(value, data.data, undefined, isRendererOnly), meta: undefined });
|
|
860
973
|
if (!operatorsUrl) {
|
|
861
974
|
setLoading(false);
|
|
862
975
|
}
|
|
@@ -901,7 +1014,7 @@ export const FormEngine = ({
|
|
|
901
1014
|
}
|
|
902
1015
|
setOptions(data.data);
|
|
903
1016
|
onOptionsLoaded?.(data.data);
|
|
904
|
-
setLocalValue({ fields: fixOptions({}, data.data), meta: undefined });
|
|
1017
|
+
setLocalValue({ fields: fixOptions({}, data.data, undefined, isRendererOnly), meta: undefined });
|
|
905
1018
|
})();
|
|
906
1019
|
}
|
|
907
1020
|
}, [url, customUrl]);
|
|
@@ -925,7 +1038,7 @@ export const FormEngine = ({
|
|
|
925
1038
|
}, [operatorsUrl]);
|
|
926
1039
|
|
|
927
1040
|
useUpdateEffect(() => {
|
|
928
|
-
const fixedValue = fixOptions(value, options || {});
|
|
1041
|
+
const fixedValue = fixOptions(value, options || {}, undefined, isRendererOnly);
|
|
929
1042
|
|
|
930
1043
|
// When the value we're receiving is the one we just emitted AND fixOptions has nothing to
|
|
931
1044
|
// meaningfully add or change, skip the update. This breaks the controlled-component loop for
|
|
@@ -949,19 +1062,21 @@ export const FormEngine = ({
|
|
|
949
1062
|
}
|
|
950
1063
|
|
|
951
1064
|
setLocalValue?.({ fields: fixedValue, meta: undefined });
|
|
952
|
-
}, [JSON.stringify(options), JSON.stringify(value)]);
|
|
1065
|
+
}, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
|
|
953
1066
|
|
|
954
1067
|
const handleValueChange = useCallback(
|
|
955
1068
|
(optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
|
|
956
1069
|
setLocalValue(({ fields = {} }) => {
|
|
957
|
-
const schemaType = (options?.[optionName]
|
|
958
|
-
options?.[optionName]?.type) as TQorusType;
|
|
1070
|
+
const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
|
|
959
1071
|
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
960
1072
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
961
1073
|
const resolvedSchemaType =
|
|
962
|
-
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
963
|
-
((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
964
|
-
|
|
1074
|
+
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
1075
|
+
? ((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
1076
|
+
: (fields[optionName] as IQorusFormField)?.type === options?.[optionName]?.ui_type &&
|
|
1077
|
+
isRendererOnly((fields[optionName] as IQorusFormField)?.type)
|
|
1078
|
+
? schemaType
|
|
1079
|
+
: schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
|
|
965
1080
|
const type =
|
|
966
1081
|
_type ||
|
|
967
1082
|
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
@@ -1044,6 +1159,7 @@ export const FormEngine = ({
|
|
|
1044
1159
|
[
|
|
1045
1160
|
onSingleOptionsChange,
|
|
1046
1161
|
onDependableOptionChange,
|
|
1162
|
+
isRendererOnly,
|
|
1047
1163
|
JSON.stringify(options),
|
|
1048
1164
|
JSON.stringify(operators),
|
|
1049
1165
|
]
|
|
@@ -1164,8 +1280,7 @@ export const FormEngine = ({
|
|
|
1164
1280
|
optionName as string,
|
|
1165
1281
|
getDefaultValue(options?.[optionName as string]),
|
|
1166
1282
|
getTypeAndCanBeNull(
|
|
1167
|
-
(options?.[optionName as string]
|
|
1168
|
-
options?.[optionName as string]?.type) as TQorusType,
|
|
1283
|
+
getOptionSchemaStorageType(options?.[optionName as string], isRendererOnly),
|
|
1169
1284
|
options?.[optionName as string]?.allowed_values
|
|
1170
1285
|
).type
|
|
1171
1286
|
);
|
|
@@ -1193,36 +1308,37 @@ export const FormEngine = ({
|
|
|
1193
1308
|
return newValue;
|
|
1194
1309
|
}
|
|
1195
1310
|
|
|
1196
|
-
const
|
|
1311
|
+
const rendererType = getType(
|
|
1197
1312
|
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
1198
1313
|
operators,
|
|
1199
1314
|
(option as IQorusFormField)?.op
|
|
1200
1315
|
);
|
|
1316
|
+
const storageType = getOptionFieldStorageType(
|
|
1317
|
+
optionName,
|
|
1318
|
+
(option as IQorusFormField)?.type,
|
|
1319
|
+
options,
|
|
1320
|
+
operators,
|
|
1321
|
+
(option as IQorusFormField)?.op,
|
|
1322
|
+
isRendererOnly
|
|
1323
|
+
);
|
|
1201
1324
|
|
|
1202
1325
|
if (!isPlainObject(option)) {
|
|
1203
1326
|
return {
|
|
1204
1327
|
...newValue,
|
|
1205
1328
|
[optionName]: {
|
|
1206
|
-
type:
|
|
1329
|
+
type: storageType || rendererType,
|
|
1207
1330
|
value: option,
|
|
1208
1331
|
},
|
|
1209
1332
|
};
|
|
1210
1333
|
}
|
|
1211
1334
|
|
|
1212
|
-
// any/auto: preserve the user-picked type; otherwise normalize to the
|
|
1213
|
-
// schema type (ui_type wins) so rendering and validation agree.
|
|
1214
|
-
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
1215
|
-
const effectiveType =
|
|
1216
|
-
isAnyLike && (option as IQorusFormField)?.type ?
|
|
1217
|
-
(option as IQorusFormField).type
|
|
1218
|
-
: schemaType;
|
|
1219
|
-
|
|
1220
1335
|
return {
|
|
1221
1336
|
...newValue,
|
|
1222
|
-
[optionName]: { ...(option as IQorusFormField), type:
|
|
1337
|
+
[optionName]: { ...(option as IQorusFormField), type: storageType || rendererType },
|
|
1223
1338
|
};
|
|
1224
1339
|
}, {});
|
|
1225
1340
|
}, [
|
|
1341
|
+
isRendererOnly,
|
|
1226
1342
|
JSON.stringify(fixedValue),
|
|
1227
1343
|
JSON.stringify(options),
|
|
1228
1344
|
unavailableOptionsCount.current,
|
|
@@ -1319,10 +1435,14 @@ export const FormEngine = ({
|
|
|
1319
1435
|
const fields: IFormFieldValidityData[] = reduce(
|
|
1320
1436
|
availableOptions,
|
|
1321
1437
|
(result, option, optionName) => {
|
|
1322
|
-
const type =
|
|
1323
|
-
|
|
1324
|
-
(
|
|
1325
|
-
|
|
1438
|
+
const type = getOptionFieldStorageType(
|
|
1439
|
+
optionName,
|
|
1440
|
+
(option as IQorusFormField).type,
|
|
1441
|
+
options,
|
|
1442
|
+
operators,
|
|
1443
|
+
(option as IQorusFormField).op,
|
|
1444
|
+
isRendererOnly
|
|
1445
|
+
);
|
|
1326
1446
|
const optionValue = (option as IQorusFormField).value;
|
|
1327
1447
|
|
|
1328
1448
|
const isRequired = options?.[optionName]?.required;
|
|
@@ -1364,8 +1484,10 @@ export const FormEngine = ({
|
|
|
1364
1484
|
invalidFields,
|
|
1365
1485
|
};
|
|
1366
1486
|
}, [
|
|
1487
|
+
isRendererOnly,
|
|
1367
1488
|
JSON.stringify(availableOptions),
|
|
1368
1489
|
JSON.stringify(options),
|
|
1490
|
+
JSON.stringify(operators),
|
|
1369
1491
|
JSON.stringify(localValue.fields),
|
|
1370
1492
|
]);
|
|
1371
1493
|
|
|
@@ -1384,8 +1506,14 @@ export const FormEngine = ({
|
|
|
1384
1506
|
showInvalidOptionsOnly &&
|
|
1385
1507
|
isOptionValid(
|
|
1386
1508
|
optionName,
|
|
1387
|
-
(
|
|
1388
|
-
|
|
1509
|
+
getOptionFieldStorageType(
|
|
1510
|
+
optionName,
|
|
1511
|
+
(option as IQorusFormField).type,
|
|
1512
|
+
options,
|
|
1513
|
+
operators,
|
|
1514
|
+
(option as IQorusFormField).op,
|
|
1515
|
+
isRendererOnly
|
|
1516
|
+
),
|
|
1389
1517
|
(option as IQorusFormField).value
|
|
1390
1518
|
)
|
|
1391
1519
|
) {
|
|
@@ -1395,7 +1523,13 @@ export const FormEngine = ({
|
|
|
1395
1523
|
},
|
|
1396
1524
|
{}
|
|
1397
1525
|
);
|
|
1398
|
-
}, [
|
|
1526
|
+
}, [
|
|
1527
|
+
showInvalidOptionsOnly,
|
|
1528
|
+
isRendererOnly,
|
|
1529
|
+
JSON.stringify(availableOptions),
|
|
1530
|
+
JSON.stringify(options),
|
|
1531
|
+
JSON.stringify(operators),
|
|
1532
|
+
]);
|
|
1399
1533
|
|
|
1400
1534
|
// Read-first STATUS / BOX for one option — lifted to component scope so the
|
|
1401
1535
|
// status boxes (renderCompact) and the header's "needs attention" count share
|
|
@@ -1415,7 +1549,14 @@ export const FormEngine = ({
|
|
|
1415
1549
|
(name: string, hidden = false): TReadFirstStatus => {
|
|
1416
1550
|
if (hidden) return 'optional';
|
|
1417
1551
|
const schema = options?.[name];
|
|
1418
|
-
const type = (
|
|
1552
|
+
const type = getOptionFieldStorageType(
|
|
1553
|
+
name,
|
|
1554
|
+
(availableOptions as TQorusForm)?.[name]?.type,
|
|
1555
|
+
options,
|
|
1556
|
+
operators,
|
|
1557
|
+
(availableOptions as TQorusForm)?.[name]?.op,
|
|
1558
|
+
isRendererOnly
|
|
1559
|
+
);
|
|
1419
1560
|
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1420
1561
|
const empty = isOptionValueEmpty(value);
|
|
1421
1562
|
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
@@ -1437,8 +1578,10 @@ export const FormEngine = ({
|
|
|
1437
1578
|
});
|
|
1438
1579
|
},
|
|
1439
1580
|
[
|
|
1581
|
+
isRendererOnly,
|
|
1440
1582
|
JSON.stringify(options),
|
|
1441
1583
|
JSON.stringify(availableOptions),
|
|
1584
|
+
JSON.stringify(operators),
|
|
1442
1585
|
isOptionValid,
|
|
1443
1586
|
requiredGroupsInfo,
|
|
1444
1587
|
schemaMsgIntent,
|
|
@@ -1541,6 +1684,32 @@ export const FormEngine = ({
|
|
|
1541
1684
|
[expandMode]
|
|
1542
1685
|
);
|
|
1543
1686
|
|
|
1687
|
+
// --- Caller-named starting rows (opt-in) ----------------------------------
|
|
1688
|
+
// `initialExpandedOptions` lets a consumer name the rows that must already
|
|
1689
|
+
// be open when the form first paints — the case where the expanded row is
|
|
1690
|
+
// part of an address rather than a click (see the prop's docs).
|
|
1691
|
+
//
|
|
1692
|
+
// One-shot for the same reason autofocus is: it fires on the first render
|
|
1693
|
+
// that actually has rows (so an async-loaded schema is covered) and then
|
|
1694
|
+
// never again, so a row the user collapses afterwards stays collapsed and a
|
|
1695
|
+
// later schema reload does not reopen it.
|
|
1696
|
+
const hasAppliedInitialExpansionRef = useRef(false);
|
|
1697
|
+
useEffect(() => {
|
|
1698
|
+
if (!initialExpandedOptions?.length || !compact || hasAppliedInitialExpansionRef.current) {
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const names = Object.keys(availableOptions);
|
|
1702
|
+
if (!names.length) {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
hasAppliedInitialExpansionRef.current = true;
|
|
1706
|
+
const toExpand = initialExpandedOptions.filter((name) => names.includes(name));
|
|
1707
|
+
if (!toExpand.length) {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
setExpandedOptions((prev) => [...prev, ...toExpand.filter((name) => !prev.includes(name))]);
|
|
1711
|
+
}, [initialExpandedOptions, compact, availableOptions]);
|
|
1712
|
+
|
|
1544
1713
|
// --- First-attention-field autofocus (opt-in) -----------------------------
|
|
1545
1714
|
// With `autoFocusFirstRequired`, drop the user straight into the first field
|
|
1546
1715
|
// they must fix. We reuse the engine's own ordering (`availableOptions`), the
|
|
@@ -1678,10 +1847,10 @@ export const FormEngine = ({
|
|
|
1678
1847
|
next[optionName] = value as IQorusFormField;
|
|
1679
1848
|
}
|
|
1680
1849
|
});
|
|
1681
|
-
return { fields: fixOptions(next, options || {}, operators), meta: undefined };
|
|
1850
|
+
return { fields: fixOptions(next, options || {}, operators, isRendererOnly), meta: undefined };
|
|
1682
1851
|
});
|
|
1683
1852
|
setRequiredOnly(false);
|
|
1684
|
-
}, [JSON.stringify(options), JSON.stringify(operators)]);
|
|
1853
|
+
}, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
|
|
1685
1854
|
|
|
1686
1855
|
const getCustomMenuTemplateItems = useCallback<(optionName: string) => TCustomTemplateItems>(
|
|
1687
1856
|
(optionName) => {
|
|
@@ -1711,6 +1880,25 @@ export const FormEngine = ({
|
|
|
1711
1880
|
suppressSchemaMessages?: boolean
|
|
1712
1881
|
) => {
|
|
1713
1882
|
const operatorParts = fixOperatorValue(other.op);
|
|
1883
|
+
// The RENDERER type for this row — which editor mounts. Distinct from the
|
|
1884
|
+
// storage type on the value envelope: a `richtext` field stores a string
|
|
1885
|
+
// but must render the richtext editor, so `ui_type` wins here.
|
|
1886
|
+
//
|
|
1887
|
+
// The exception is an any-like `ui_type`. Those schemas say
|
|
1888
|
+
// `ui_type: 'any'` precisely so the user can pick a concrete type, and
|
|
1889
|
+
// that pick lives on the field's stored `type` — letting 'any' win would
|
|
1890
|
+
// pin the row to the untyped editor forever.
|
|
1891
|
+
//
|
|
1892
|
+
// The trailing fallbacks keep the row rendering when a server-driven
|
|
1893
|
+
// schema has not arrived yet (`type` undefined) instead of crashing.
|
|
1894
|
+
const schemaUiType = options?.[optionName]?.ui_type as TQorusType;
|
|
1895
|
+
const uiTypeIsAnyLike = schemaUiType === 'any' || schemaUiType === 'auto';
|
|
1896
|
+
const resolvedType =
|
|
1897
|
+
(schemaUiType && !uiTypeIsAnyLike ? schemaUiType : undefined) ||
|
|
1898
|
+
type ||
|
|
1899
|
+
schemaUiType ||
|
|
1900
|
+
(options?.[optionName]?.type as TQorusType) ||
|
|
1901
|
+
'any';
|
|
1714
1902
|
return (
|
|
1715
1903
|
<>
|
|
1716
1904
|
{(() => {
|
|
@@ -1852,15 +2040,18 @@ export const FormEngine = ({
|
|
|
1852
2040
|
// (DPQL text mode); opt out per-form via `templateFieldProps`.
|
|
1853
2041
|
allowTextExpressions
|
|
1854
2042
|
allowCustomValues={
|
|
1855
|
-
options?.[optionName]?.supports_custom_values !== false &&
|
|
2043
|
+
options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any'
|
|
1856
2044
|
}
|
|
1857
2045
|
templates={templates.value}
|
|
1858
2046
|
{...getTypeAndCanBeNull(
|
|
1859
|
-
type
|
|
2047
|
+
// The RENDERER type picks the editor — the storage type lives on
|
|
2048
|
+
// the value envelope. Passing storage here rendered a `richtext`
|
|
2049
|
+
// field as a plain string input.
|
|
2050
|
+
resolvedType,
|
|
1860
2051
|
options?.[optionName]?.allowed_values,
|
|
1861
2052
|
other.op
|
|
1862
2053
|
)}
|
|
1863
|
-
ui_type={
|
|
2054
|
+
ui_type={resolvedType}
|
|
1864
2055
|
name={optionName}
|
|
1865
2056
|
uniqueName={`${uniqueName ? `${uniqueName}.` : `${name ? `${name}.` : ''}`}${optionName}`}
|
|
1866
2057
|
onChange={
|
|
@@ -1901,7 +2092,7 @@ export const FormEngine = ({
|
|
|
1901
2092
|
schema={options || {}}
|
|
1902
2093
|
allOptions={availableOptions}
|
|
1903
2094
|
name={optionName}
|
|
1904
|
-
option={{ type, ...other }}
|
|
2095
|
+
option={{ type: resolvedType, ...other }}
|
|
1905
2096
|
getType={getTypeForOption}
|
|
1906
2097
|
/>
|
|
1907
2098
|
{operators && size(operators) && size(other.op) ?
|
|
@@ -1916,7 +2107,7 @@ export const FormEngine = ({
|
|
|
1916
2107
|
intent='info'
|
|
1917
2108
|
label={
|
|
1918
2109
|
other.value ?
|
|
1919
|
-
|
|
2110
|
+
resolvedType === 'richtext' ?
|
|
1920
2111
|
richtextToString(other.value)
|
|
1921
2112
|
: JSON.stringify(other.value)
|
|
1922
2113
|
: ''
|
|
@@ -2009,6 +2200,8 @@ export const FormEngine = ({
|
|
|
2009
2200
|
getTypeForOption,
|
|
2010
2201
|
isOptionValid,
|
|
2011
2202
|
confirmAction,
|
|
2203
|
+
optionActions,
|
|
2204
|
+
collapseOptionActions,
|
|
2012
2205
|
renderOption,
|
|
2013
2206
|
theme,
|
|
2014
2207
|
cText,
|
|
@@ -2054,6 +2247,8 @@ export const FormEngine = ({
|
|
|
2054
2247
|
getTypeForOption,
|
|
2055
2248
|
isOptionValid,
|
|
2056
2249
|
confirmAction,
|
|
2250
|
+
optionActions,
|
|
2251
|
+
collapseOptionActions,
|
|
2057
2252
|
renderOption,
|
|
2058
2253
|
theme,
|
|
2059
2254
|
cText,
|
|
@@ -2190,7 +2385,14 @@ export const FormEngine = ({
|
|
|
2190
2385
|
const isFieldInvalid = (name: string) =>
|
|
2191
2386
|
!isOptionValid(
|
|
2192
2387
|
name,
|
|
2193
|
-
(
|
|
2388
|
+
getOptionFieldStorageType(
|
|
2389
|
+
name,
|
|
2390
|
+
(shownOptions as TQorusForm)[name]?.type,
|
|
2391
|
+
options,
|
|
2392
|
+
operators,
|
|
2393
|
+
(shownOptions as TQorusForm)[name]?.op,
|
|
2394
|
+
isRendererOnly
|
|
2395
|
+
),
|
|
2194
2396
|
(shownOptions as TQorusForm)[name]?.value
|
|
2195
2397
|
);
|
|
2196
2398
|
const comparator = (a: { name: string }, b: { name: string }): number => {
|
|
@@ -2299,7 +2501,7 @@ export const FormEngine = ({
|
|
|
2299
2501
|
optionField={
|
|
2300
2502
|
entry.hidden ?
|
|
2301
2503
|
({
|
|
2302
|
-
type: (options?.[entry.name]
|
|
2504
|
+
type: getOptionSchemaStorageType(options?.[entry.name], isRendererOnly),
|
|
2303
2505
|
value: undefined,
|
|
2304
2506
|
} as IQorusFormField)
|
|
2305
2507
|
: ((shownOptions as TQorusForm)[entry.name] as IQorusFormField)
|
|
@@ -2760,13 +2962,11 @@ export const FormEngine = ({
|
|
|
2760
2962
|
// option's schema as context). The consumer (the IDE) injects
|
|
2761
2963
|
// it; same factory pattern as the ExpressionBuilder's
|
|
2762
2964
|
// `extraActions`.
|
|
2763
|
-
...(
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
})
|
|
2769
|
-
: (optionActions ?? [])),
|
|
2965
|
+
...resolveOptionActions(optionActions, {
|
|
2966
|
+
name: optionName,
|
|
2967
|
+
schema: options[optionName],
|
|
2968
|
+
value: availableOptions?.[optionName] as TOption,
|
|
2969
|
+
}),
|
|
2770
2970
|
{
|
|
2771
2971
|
size: 'tiny',
|
|
2772
2972
|
icon: 'FullscreenLine',
|