@qoretechnologies/reqraft 0.10.16 → 0.10.18
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 +37 -11
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +187 -116
- 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/package.json +3 -3
- package/src/components/form/engine/CompactRow.tsx +139 -0
- package/src/components/form/engine/FormEngine.tsx +229 -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
|
@@ -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,8 @@ 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';
|
|
44
43
|
import { cloneDeep, findKey, flatten, forEach, isEqual, isPlainObject, last } from 'lodash';
|
|
45
44
|
import isArray from 'lodash/isArray';
|
|
46
45
|
import map from 'lodash/map';
|
|
@@ -245,12 +244,16 @@ const StyledCommitDock = styled.div<{ $bg: string; $border: string }>`
|
|
|
245
244
|
`;
|
|
246
245
|
|
|
247
246
|
export const getType = (
|
|
248
|
-
type
|
|
247
|
+
type?: TQorusType | TQorusType[],
|
|
249
248
|
operators?: IOperatorsSchema,
|
|
250
249
|
operator?: TOperatorValue
|
|
251
250
|
): TQorusType => {
|
|
252
251
|
const finalType = getTypeFromOperator(operators, fixOperatorValue(operator)) || type;
|
|
253
|
-
|
|
252
|
+
const resolvedType = isArray(finalType) ? finalType[0] : finalType;
|
|
253
|
+
|
|
254
|
+
// A value can be received before its server-driven option schema. Keep the
|
|
255
|
+
// renderer type-safe during that transition without guessing a concrete type.
|
|
256
|
+
return (resolvedType || 'any') as TQorusType;
|
|
254
257
|
};
|
|
255
258
|
|
|
256
259
|
const getTypeFromOperator = (
|
|
@@ -267,6 +270,38 @@ export const fixOperatorValue = (operator: TOperatorValue): (string | null | und
|
|
|
267
270
|
return isArray(operator) ? operator : [operator];
|
|
268
271
|
};
|
|
269
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The renderer-only predicate. Defaults to reqraft's built-ins; a FormEngine
|
|
275
|
+
* instance passes its own (built-ins + the `rendererOnlyUiTypes` prop) so a
|
|
276
|
+
* consumer's injected editors are recognised too. See `./rendererTypes`.
|
|
277
|
+
*/
|
|
278
|
+
type TRendererOnlyCheck = (type?: TQorusType | TQorusType[]) => boolean;
|
|
279
|
+
|
|
280
|
+
const getOptionSchemaStorageType = (
|
|
281
|
+
option?: TQorusFormFieldSchema,
|
|
282
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
283
|
+
): TQorusType =>
|
|
284
|
+
((isRendererOnly(option?.ui_type)
|
|
285
|
+
? option?.type || option?.ui_type
|
|
286
|
+
: option?.ui_type || option?.type) || 'any') as TQorusType;
|
|
287
|
+
|
|
288
|
+
const getOptionFieldStorageType = (
|
|
289
|
+
optionName: string,
|
|
290
|
+
fieldType: TQorusType | TQorusType[] | undefined,
|
|
291
|
+
schema?: IQorusFormSchema,
|
|
292
|
+
operators?: IOperatorsSchema,
|
|
293
|
+
operatorData?: TOperatorValue,
|
|
294
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
295
|
+
): TQorusType => {
|
|
296
|
+
const schemaOption = schema?.[optionName];
|
|
297
|
+
const storedType =
|
|
298
|
+
fieldType && !(fieldType === schemaOption?.ui_type && isRendererOnly(fieldType))
|
|
299
|
+
? fieldType
|
|
300
|
+
: getOptionSchemaStorageType(schemaOption, isRendererOnly);
|
|
301
|
+
|
|
302
|
+
return getType(storedType as TQorusType, operators, operatorData);
|
|
303
|
+
};
|
|
304
|
+
|
|
270
305
|
export const hasRequiredOptions = (options: IQorusFormSchema = {}) => {
|
|
271
306
|
return !!findKey(options, (option) => option.required);
|
|
272
307
|
};
|
|
@@ -278,11 +313,19 @@ export const OptionsContext = createContext<{
|
|
|
278
313
|
|
|
279
314
|
export const fixOptions = (
|
|
280
315
|
value: TQorusForm | TQorusFlatForm = {},
|
|
281
|
-
options
|
|
282
|
-
operators?: IOperatorsSchema
|
|
316
|
+
options?: IQorusFormSchema,
|
|
317
|
+
operators?: IOperatorsSchema,
|
|
318
|
+
isRendererOnly: TRendererOnlyCheck = isRendererOnlyUiType
|
|
283
319
|
): TQorusForm => {
|
|
284
320
|
const fixedValue = cloneDeep(value);
|
|
285
321
|
|
|
322
|
+
// Server-driven schemas can arrive after the persisted value. Preserve that
|
|
323
|
+
// value verbatim until its schema is available instead of manufacturing
|
|
324
|
+
// partially typed form fields that can crash the following render.
|
|
325
|
+
if (!size(options)) {
|
|
326
|
+
return fixedValue as TQorusForm;
|
|
327
|
+
}
|
|
328
|
+
|
|
286
329
|
forEach(options, (option, name) => {
|
|
287
330
|
if (
|
|
288
331
|
option.value ||
|
|
@@ -292,7 +335,7 @@ export const fixOptions = (
|
|
|
292
335
|
) {
|
|
293
336
|
let obj: IQorusFormField;
|
|
294
337
|
const type = getType(
|
|
295
|
-
(option
|
|
338
|
+
getOptionSchemaStorageType(option, isRendererOnly),
|
|
296
339
|
operators,
|
|
297
340
|
(fixedValue[name] as IQorusFormField)?.op
|
|
298
341
|
);
|
|
@@ -328,20 +371,35 @@ export const fixOptions = (
|
|
|
328
371
|
let newOption = option as IQorusFormField;
|
|
329
372
|
|
|
330
373
|
if (!isPlainObject(newOption) || !(newOption as IQorusFormField)?.type) {
|
|
374
|
+
const isUntypedEnvelope = isPlainObject(newOption) && 'value' in newOption;
|
|
375
|
+
const untypedFieldValue =
|
|
376
|
+
isUntypedEnvelope ? (newOption as IQorusFormField).value : newOption;
|
|
377
|
+
// Spread the original envelope first so every key it carried survives —
|
|
378
|
+
// rebuilding from just {type, value} silently dropped `op`, `is_expression`
|
|
379
|
+
// and anything else a field needs to render, which emptied the card.
|
|
331
380
|
const fixedOption: IQorusFormField = {
|
|
381
|
+
...(isPlainObject(newOption) ? (newOption as IQorusFormField) : {}),
|
|
332
382
|
type: getType(
|
|
333
|
-
(options?.[optionName]
|
|
383
|
+
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
334
384
|
operators,
|
|
335
385
|
(newOption as IQorusFormField)?.op
|
|
336
386
|
),
|
|
337
|
-
value:
|
|
387
|
+
value: untypedFieldValue,
|
|
338
388
|
};
|
|
339
389
|
|
|
340
|
-
if ((newOption as IQorusFormField)?.is_expression) {
|
|
341
|
-
fixedOption.is_expression = true;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
390
|
newOption = fixedOption;
|
|
391
|
+
} else if (
|
|
392
|
+
newOption.type === options?.[optionName]?.ui_type &&
|
|
393
|
+
isRendererOnly(newOption.type)
|
|
394
|
+
) {
|
|
395
|
+
newOption = {
|
|
396
|
+
...newOption,
|
|
397
|
+
type: getType(
|
|
398
|
+
getOptionSchemaStorageType(options?.[optionName], isRendererOnly),
|
|
399
|
+
operators,
|
|
400
|
+
newOption.op
|
|
401
|
+
),
|
|
402
|
+
};
|
|
345
403
|
}
|
|
346
404
|
|
|
347
405
|
if (
|
|
@@ -411,7 +469,7 @@ export const flattenOptions = (options: TQorusForm): TQorusFlatForm => {
|
|
|
411
469
|
};
|
|
412
470
|
|
|
413
471
|
export const getTypeAndCanBeNull = (
|
|
414
|
-
type
|
|
472
|
+
type?: TQorusType | TQorusType[],
|
|
415
473
|
allowed_values?: any[],
|
|
416
474
|
operatorData?: TOperatorValue,
|
|
417
475
|
operators?: IOperatorsSchema
|
|
@@ -546,19 +604,36 @@ export interface IFormEngineProps extends Omit<IReqoreCollectionProps, 'onChange
|
|
|
546
604
|
* option's name/schema/value (the context the IDE's `AiAssistanceAction`
|
|
547
605
|
* captures); the consumer injects the button, reqraft stays AI-free.
|
|
548
606
|
*/
|
|
549
|
-
optionActions?:
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
607
|
+
optionActions?: TOptionActions;
|
|
608
|
+
/**
|
|
609
|
+
* Whether per-option injected actions collapse into the row's overflow menu
|
|
610
|
+
* instead of rendering as inline buttons.
|
|
611
|
+
*
|
|
612
|
+
* `'auto'` (default) collapses when the device cannot hover or the viewport is
|
|
613
|
+
* narrow — on a phone a hover-gated action would otherwise be unreachable, and
|
|
614
|
+
* a row has no width for a button strip. `'always'` / `'never'` force it, which
|
|
615
|
+
* consumers can use for a known-mobile surface and stories use to capture the
|
|
616
|
+
* collapsed state deterministically.
|
|
617
|
+
*
|
|
618
|
+
* Actions beyond `MAX_INLINE_OPTION_ACTIONS` always overflow into the menu, so
|
|
619
|
+
* a consumer injecting many actions can never blow out the row.
|
|
620
|
+
*/
|
|
621
|
+
optionActionsCollapse?: 'auto' | 'always' | 'never';
|
|
556
622
|
/**
|
|
557
623
|
* SEAM (reqraft): consumer-injected field editors for types reqraft doesn't
|
|
558
624
|
* ship (IDE domain fields). Keyed by field `type`/`ui_type`; forwarded through
|
|
559
625
|
* `TemplateField` to the `AutoFormField` override seam.
|
|
560
626
|
*/
|
|
561
627
|
componentOverrides?: Record<string, React.FC<any>>;
|
|
628
|
+
/**
|
|
629
|
+
* The `ui_type` names among `componentOverrides` that select a bespoke editor
|
|
630
|
+
* but store their value as the schema's plainer `type` (a `cron` editor stores
|
|
631
|
+
* a string). Declaring them here keeps the field's stored `type` correct —
|
|
632
|
+
* without it the renderer name is written into the value and validation and
|
|
633
|
+
* round-tripping both break. Merged with reqraft's own built-in list, so a
|
|
634
|
+
* consumer's new editor no longer needs a reqraft release to be handled.
|
|
635
|
+
*/
|
|
636
|
+
rendererOnlyUiTypes?: string[];
|
|
562
637
|
|
|
563
638
|
/**
|
|
564
639
|
* Bag of values forwarded from an outer FormEngine scope, used as a
|
|
@@ -644,12 +719,35 @@ export const FormEngine = ({
|
|
|
644
719
|
optionsLoader,
|
|
645
720
|
onValidityChange,
|
|
646
721
|
optionActions,
|
|
722
|
+
optionActionsCollapse = 'auto',
|
|
647
723
|
componentOverrides,
|
|
724
|
+
rendererOnlyUiTypes,
|
|
648
725
|
inheritedFromParent,
|
|
649
726
|
autoFocusFirstRequired,
|
|
650
727
|
initialExpandedOptions,
|
|
651
728
|
...rest
|
|
652
729
|
}: IFormEngineProps) => {
|
|
730
|
+
// Built-ins + whatever the consumer declared for its own injected editors.
|
|
731
|
+
const isRendererOnly = useMemo(
|
|
732
|
+
() => createRendererOnlyUiTypeCheck(rendererOnlyUiTypes),
|
|
733
|
+
[JSON.stringify(rendererOnlyUiTypes)]
|
|
734
|
+
);
|
|
735
|
+
// Pointer CAPABILITY and viewport WIDTH are different questions and both
|
|
736
|
+
// matter here: a hover-gated action is unreachable without a hover, and a
|
|
737
|
+
// phone-width row has no space for a button strip either way. Both now come
|
|
738
|
+
// from Reqore's context (one subscription for the whole app) rather than a
|
|
739
|
+
// reqraft-local matchMedia hook.
|
|
740
|
+
//
|
|
741
|
+
// `isHoverCapable` defaults to `true` in Reqore, but fall back explicitly so
|
|
742
|
+
// an older Reqore — where the property does not exist — degrades to "this
|
|
743
|
+
// pointer hovers" (the pre-existing behaviour) instead of collapsing every
|
|
744
|
+
// action into a menu for everyone.
|
|
745
|
+
const isHoverCapable = useReqoreProperty('isHoverCapable') ?? true;
|
|
746
|
+
const isMobile = useReqoreProperty('isMobile');
|
|
747
|
+
const collapseOptionActions =
|
|
748
|
+
optionActionsCollapse === 'always' ? true
|
|
749
|
+
: optionActionsCollapse === 'never' ? false
|
|
750
|
+
: !isHoverCapable || !!isMobile;
|
|
653
751
|
const [options, setOptions] = useState<IQorusFormSchema | undefined>(rest?.options || undefined);
|
|
654
752
|
// optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
|
|
655
753
|
const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
|
|
@@ -785,7 +883,7 @@ export const FormEngine = ({
|
|
|
785
883
|
fields: TQorusForm | TQorusFlatForm;
|
|
786
884
|
meta?: IOptionsOnChangeMeta;
|
|
787
885
|
}>(() => ({
|
|
788
|
-
fields: fixOptions(value, options || {}),
|
|
886
|
+
fields: fixOptions(value, options || {}, undefined, isRendererOnly),
|
|
789
887
|
meta: undefined,
|
|
790
888
|
}));
|
|
791
889
|
const originalValue = useRef<any>();
|
|
@@ -878,7 +976,7 @@ export const FormEngine = ({
|
|
|
878
976
|
setOptions({});
|
|
879
977
|
return;
|
|
880
978
|
}
|
|
881
|
-
setLocalValue({ fields: fixOptions(value, data.data), meta: undefined });
|
|
979
|
+
setLocalValue({ fields: fixOptions(value, data.data, undefined, isRendererOnly), meta: undefined });
|
|
882
980
|
if (!operatorsUrl) {
|
|
883
981
|
setLoading(false);
|
|
884
982
|
}
|
|
@@ -923,7 +1021,7 @@ export const FormEngine = ({
|
|
|
923
1021
|
}
|
|
924
1022
|
setOptions(data.data);
|
|
925
1023
|
onOptionsLoaded?.(data.data);
|
|
926
|
-
setLocalValue({ fields: fixOptions({}, data.data), meta: undefined });
|
|
1024
|
+
setLocalValue({ fields: fixOptions({}, data.data, undefined, isRendererOnly), meta: undefined });
|
|
927
1025
|
})();
|
|
928
1026
|
}
|
|
929
1027
|
}, [url, customUrl]);
|
|
@@ -947,7 +1045,7 @@ export const FormEngine = ({
|
|
|
947
1045
|
}, [operatorsUrl]);
|
|
948
1046
|
|
|
949
1047
|
useUpdateEffect(() => {
|
|
950
|
-
const fixedValue = fixOptions(value, options || {});
|
|
1048
|
+
const fixedValue = fixOptions(value, options || {}, undefined, isRendererOnly);
|
|
951
1049
|
|
|
952
1050
|
// When the value we're receiving is the one we just emitted AND fixOptions has nothing to
|
|
953
1051
|
// meaningfully add or change, skip the update. This breaks the controlled-component loop for
|
|
@@ -971,19 +1069,21 @@ export const FormEngine = ({
|
|
|
971
1069
|
}
|
|
972
1070
|
|
|
973
1071
|
setLocalValue?.({ fields: fixedValue, meta: undefined });
|
|
974
|
-
}, [JSON.stringify(options), JSON.stringify(value)]);
|
|
1072
|
+
}, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
|
|
975
1073
|
|
|
976
1074
|
const handleValueChange = useCallback(
|
|
977
1075
|
(optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
|
|
978
1076
|
setLocalValue(({ fields = {} }) => {
|
|
979
|
-
const schemaType = (options?.[optionName]
|
|
980
|
-
options?.[optionName]?.type) as TQorusType;
|
|
1077
|
+
const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
|
|
981
1078
|
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
982
1079
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
983
1080
|
const resolvedSchemaType =
|
|
984
|
-
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
985
|
-
((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
986
|
-
|
|
1081
|
+
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
1082
|
+
? ((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
1083
|
+
: (fields[optionName] as IQorusFormField)?.type === options?.[optionName]?.ui_type &&
|
|
1084
|
+
isRendererOnly((fields[optionName] as IQorusFormField)?.type)
|
|
1085
|
+
? schemaType
|
|
1086
|
+
: schemaType || ((fields[optionName] as IQorusFormField)?.type as TQorusType);
|
|
987
1087
|
const type =
|
|
988
1088
|
_type ||
|
|
989
1089
|
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
@@ -1066,6 +1166,7 @@ export const FormEngine = ({
|
|
|
1066
1166
|
[
|
|
1067
1167
|
onSingleOptionsChange,
|
|
1068
1168
|
onDependableOptionChange,
|
|
1169
|
+
isRendererOnly,
|
|
1069
1170
|
JSON.stringify(options),
|
|
1070
1171
|
JSON.stringify(operators),
|
|
1071
1172
|
]
|
|
@@ -1186,8 +1287,7 @@ export const FormEngine = ({
|
|
|
1186
1287
|
optionName as string,
|
|
1187
1288
|
getDefaultValue(options?.[optionName as string]),
|
|
1188
1289
|
getTypeAndCanBeNull(
|
|
1189
|
-
(options?.[optionName as string]
|
|
1190
|
-
options?.[optionName as string]?.type) as TQorusType,
|
|
1290
|
+
getOptionSchemaStorageType(options?.[optionName as string], isRendererOnly),
|
|
1191
1291
|
options?.[optionName as string]?.allowed_values
|
|
1192
1292
|
).type
|
|
1193
1293
|
);
|
|
@@ -1215,36 +1315,37 @@ export const FormEngine = ({
|
|
|
1215
1315
|
return newValue;
|
|
1216
1316
|
}
|
|
1217
1317
|
|
|
1218
|
-
const
|
|
1318
|
+
const rendererType = getType(
|
|
1219
1319
|
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
1220
1320
|
operators,
|
|
1221
1321
|
(option as IQorusFormField)?.op
|
|
1222
1322
|
);
|
|
1323
|
+
const storageType = getOptionFieldStorageType(
|
|
1324
|
+
optionName,
|
|
1325
|
+
(option as IQorusFormField)?.type,
|
|
1326
|
+
options,
|
|
1327
|
+
operators,
|
|
1328
|
+
(option as IQorusFormField)?.op,
|
|
1329
|
+
isRendererOnly
|
|
1330
|
+
);
|
|
1223
1331
|
|
|
1224
1332
|
if (!isPlainObject(option)) {
|
|
1225
1333
|
return {
|
|
1226
1334
|
...newValue,
|
|
1227
1335
|
[optionName]: {
|
|
1228
|
-
type:
|
|
1336
|
+
type: storageType || rendererType,
|
|
1229
1337
|
value: option,
|
|
1230
1338
|
},
|
|
1231
1339
|
};
|
|
1232
1340
|
}
|
|
1233
1341
|
|
|
1234
|
-
// any/auto: preserve the user-picked type; otherwise normalize to the
|
|
1235
|
-
// schema type (ui_type wins) so rendering and validation agree.
|
|
1236
|
-
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
1237
|
-
const effectiveType =
|
|
1238
|
-
isAnyLike && (option as IQorusFormField)?.type ?
|
|
1239
|
-
(option as IQorusFormField).type
|
|
1240
|
-
: schemaType;
|
|
1241
|
-
|
|
1242
1342
|
return {
|
|
1243
1343
|
...newValue,
|
|
1244
|
-
[optionName]: { ...(option as IQorusFormField), type:
|
|
1344
|
+
[optionName]: { ...(option as IQorusFormField), type: storageType || rendererType },
|
|
1245
1345
|
};
|
|
1246
1346
|
}, {});
|
|
1247
1347
|
}, [
|
|
1348
|
+
isRendererOnly,
|
|
1248
1349
|
JSON.stringify(fixedValue),
|
|
1249
1350
|
JSON.stringify(options),
|
|
1250
1351
|
unavailableOptionsCount.current,
|
|
@@ -1341,10 +1442,14 @@ export const FormEngine = ({
|
|
|
1341
1442
|
const fields: IFormFieldValidityData[] = reduce(
|
|
1342
1443
|
availableOptions,
|
|
1343
1444
|
(result, option, optionName) => {
|
|
1344
|
-
const type =
|
|
1345
|
-
|
|
1346
|
-
(
|
|
1347
|
-
|
|
1445
|
+
const type = getOptionFieldStorageType(
|
|
1446
|
+
optionName,
|
|
1447
|
+
(option as IQorusFormField).type,
|
|
1448
|
+
options,
|
|
1449
|
+
operators,
|
|
1450
|
+
(option as IQorusFormField).op,
|
|
1451
|
+
isRendererOnly
|
|
1452
|
+
);
|
|
1348
1453
|
const optionValue = (option as IQorusFormField).value;
|
|
1349
1454
|
|
|
1350
1455
|
const isRequired = options?.[optionName]?.required;
|
|
@@ -1386,8 +1491,10 @@ export const FormEngine = ({
|
|
|
1386
1491
|
invalidFields,
|
|
1387
1492
|
};
|
|
1388
1493
|
}, [
|
|
1494
|
+
isRendererOnly,
|
|
1389
1495
|
JSON.stringify(availableOptions),
|
|
1390
1496
|
JSON.stringify(options),
|
|
1497
|
+
JSON.stringify(operators),
|
|
1391
1498
|
JSON.stringify(localValue.fields),
|
|
1392
1499
|
]);
|
|
1393
1500
|
|
|
@@ -1406,8 +1513,14 @@ export const FormEngine = ({
|
|
|
1406
1513
|
showInvalidOptionsOnly &&
|
|
1407
1514
|
isOptionValid(
|
|
1408
1515
|
optionName,
|
|
1409
|
-
(
|
|
1410
|
-
|
|
1516
|
+
getOptionFieldStorageType(
|
|
1517
|
+
optionName,
|
|
1518
|
+
(option as IQorusFormField).type,
|
|
1519
|
+
options,
|
|
1520
|
+
operators,
|
|
1521
|
+
(option as IQorusFormField).op,
|
|
1522
|
+
isRendererOnly
|
|
1523
|
+
),
|
|
1411
1524
|
(option as IQorusFormField).value
|
|
1412
1525
|
)
|
|
1413
1526
|
) {
|
|
@@ -1417,7 +1530,13 @@ export const FormEngine = ({
|
|
|
1417
1530
|
},
|
|
1418
1531
|
{}
|
|
1419
1532
|
);
|
|
1420
|
-
}, [
|
|
1533
|
+
}, [
|
|
1534
|
+
showInvalidOptionsOnly,
|
|
1535
|
+
isRendererOnly,
|
|
1536
|
+
JSON.stringify(availableOptions),
|
|
1537
|
+
JSON.stringify(options),
|
|
1538
|
+
JSON.stringify(operators),
|
|
1539
|
+
]);
|
|
1421
1540
|
|
|
1422
1541
|
// Read-first STATUS / BOX for one option — lifted to component scope so the
|
|
1423
1542
|
// status boxes (renderCompact) and the header's "needs attention" count share
|
|
@@ -1437,7 +1556,14 @@ export const FormEngine = ({
|
|
|
1437
1556
|
(name: string, hidden = false): TReadFirstStatus => {
|
|
1438
1557
|
if (hidden) return 'optional';
|
|
1439
1558
|
const schema = options?.[name];
|
|
1440
|
-
const type = (
|
|
1559
|
+
const type = getOptionFieldStorageType(
|
|
1560
|
+
name,
|
|
1561
|
+
(availableOptions as TQorusForm)?.[name]?.type,
|
|
1562
|
+
options,
|
|
1563
|
+
operators,
|
|
1564
|
+
(availableOptions as TQorusForm)?.[name]?.op,
|
|
1565
|
+
isRendererOnly
|
|
1566
|
+
);
|
|
1441
1567
|
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1442
1568
|
const empty = isOptionValueEmpty(value);
|
|
1443
1569
|
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
@@ -1459,8 +1585,10 @@ export const FormEngine = ({
|
|
|
1459
1585
|
});
|
|
1460
1586
|
},
|
|
1461
1587
|
[
|
|
1588
|
+
isRendererOnly,
|
|
1462
1589
|
JSON.stringify(options),
|
|
1463
1590
|
JSON.stringify(availableOptions),
|
|
1591
|
+
JSON.stringify(operators),
|
|
1464
1592
|
isOptionValid,
|
|
1465
1593
|
requiredGroupsInfo,
|
|
1466
1594
|
schemaMsgIntent,
|
|
@@ -1726,10 +1854,10 @@ export const FormEngine = ({
|
|
|
1726
1854
|
next[optionName] = value as IQorusFormField;
|
|
1727
1855
|
}
|
|
1728
1856
|
});
|
|
1729
|
-
return { fields: fixOptions(next, options || {}, operators), meta: undefined };
|
|
1857
|
+
return { fields: fixOptions(next, options || {}, operators, isRendererOnly), meta: undefined };
|
|
1730
1858
|
});
|
|
1731
1859
|
setRequiredOnly(false);
|
|
1732
|
-
}, [JSON.stringify(options), JSON.stringify(operators)]);
|
|
1860
|
+
}, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
|
|
1733
1861
|
|
|
1734
1862
|
const getCustomMenuTemplateItems = useCallback<(optionName: string) => TCustomTemplateItems>(
|
|
1735
1863
|
(optionName) => {
|
|
@@ -1759,6 +1887,25 @@ export const FormEngine = ({
|
|
|
1759
1887
|
suppressSchemaMessages?: boolean
|
|
1760
1888
|
) => {
|
|
1761
1889
|
const operatorParts = fixOperatorValue(other.op);
|
|
1890
|
+
// The RENDERER type for this row — which editor mounts. Distinct from the
|
|
1891
|
+
// storage type on the value envelope: a `richtext` field stores a string
|
|
1892
|
+
// but must render the richtext editor, so `ui_type` wins here.
|
|
1893
|
+
//
|
|
1894
|
+
// The exception is an any-like `ui_type`. Those schemas say
|
|
1895
|
+
// `ui_type: 'any'` precisely so the user can pick a concrete type, and
|
|
1896
|
+
// that pick lives on the field's stored `type` — letting 'any' win would
|
|
1897
|
+
// pin the row to the untyped editor forever.
|
|
1898
|
+
//
|
|
1899
|
+
// The trailing fallbacks keep the row rendering when a server-driven
|
|
1900
|
+
// schema has not arrived yet (`type` undefined) instead of crashing.
|
|
1901
|
+
const schemaUiType = options?.[optionName]?.ui_type as TQorusType;
|
|
1902
|
+
const uiTypeIsAnyLike = schemaUiType === 'any' || schemaUiType === 'auto';
|
|
1903
|
+
const resolvedType =
|
|
1904
|
+
(schemaUiType && !uiTypeIsAnyLike ? schemaUiType : undefined) ||
|
|
1905
|
+
type ||
|
|
1906
|
+
schemaUiType ||
|
|
1907
|
+
(options?.[optionName]?.type as TQorusType) ||
|
|
1908
|
+
'any';
|
|
1762
1909
|
return (
|
|
1763
1910
|
<>
|
|
1764
1911
|
{(() => {
|
|
@@ -1900,15 +2047,18 @@ export const FormEngine = ({
|
|
|
1900
2047
|
// (DPQL text mode); opt out per-form via `templateFieldProps`.
|
|
1901
2048
|
allowTextExpressions
|
|
1902
2049
|
allowCustomValues={
|
|
1903
|
-
options?.[optionName]?.supports_custom_values !== false &&
|
|
2050
|
+
options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any'
|
|
1904
2051
|
}
|
|
1905
2052
|
templates={templates.value}
|
|
1906
2053
|
{...getTypeAndCanBeNull(
|
|
1907
|
-
type
|
|
2054
|
+
// The RENDERER type picks the editor — the storage type lives on
|
|
2055
|
+
// the value envelope. Passing storage here rendered a `richtext`
|
|
2056
|
+
// field as a plain string input.
|
|
2057
|
+
resolvedType,
|
|
1908
2058
|
options?.[optionName]?.allowed_values,
|
|
1909
2059
|
other.op
|
|
1910
2060
|
)}
|
|
1911
|
-
ui_type={
|
|
2061
|
+
ui_type={resolvedType}
|
|
1912
2062
|
name={optionName}
|
|
1913
2063
|
uniqueName={`${uniqueName ? `${uniqueName}.` : `${name ? `${name}.` : ''}`}${optionName}`}
|
|
1914
2064
|
onChange={
|
|
@@ -1949,7 +2099,7 @@ export const FormEngine = ({
|
|
|
1949
2099
|
schema={options || {}}
|
|
1950
2100
|
allOptions={availableOptions}
|
|
1951
2101
|
name={optionName}
|
|
1952
|
-
option={{ type, ...other }}
|
|
2102
|
+
option={{ type: resolvedType, ...other }}
|
|
1953
2103
|
getType={getTypeForOption}
|
|
1954
2104
|
/>
|
|
1955
2105
|
{operators && size(operators) && size(other.op) ?
|
|
@@ -1964,7 +2114,7 @@ export const FormEngine = ({
|
|
|
1964
2114
|
intent='info'
|
|
1965
2115
|
label={
|
|
1966
2116
|
other.value ?
|
|
1967
|
-
|
|
2117
|
+
resolvedType === 'richtext' ?
|
|
1968
2118
|
richtextToString(other.value)
|
|
1969
2119
|
: JSON.stringify(other.value)
|
|
1970
2120
|
: ''
|
|
@@ -2057,6 +2207,8 @@ export const FormEngine = ({
|
|
|
2057
2207
|
getTypeForOption,
|
|
2058
2208
|
isOptionValid,
|
|
2059
2209
|
confirmAction,
|
|
2210
|
+
optionActions,
|
|
2211
|
+
collapseOptionActions,
|
|
2060
2212
|
renderOption,
|
|
2061
2213
|
theme,
|
|
2062
2214
|
cText,
|
|
@@ -2102,6 +2254,8 @@ export const FormEngine = ({
|
|
|
2102
2254
|
getTypeForOption,
|
|
2103
2255
|
isOptionValid,
|
|
2104
2256
|
confirmAction,
|
|
2257
|
+
optionActions,
|
|
2258
|
+
collapseOptionActions,
|
|
2105
2259
|
renderOption,
|
|
2106
2260
|
theme,
|
|
2107
2261
|
cText,
|
|
@@ -2238,7 +2392,14 @@ export const FormEngine = ({
|
|
|
2238
2392
|
const isFieldInvalid = (name: string) =>
|
|
2239
2393
|
!isOptionValid(
|
|
2240
2394
|
name,
|
|
2241
|
-
(
|
|
2395
|
+
getOptionFieldStorageType(
|
|
2396
|
+
name,
|
|
2397
|
+
(shownOptions as TQorusForm)[name]?.type,
|
|
2398
|
+
options,
|
|
2399
|
+
operators,
|
|
2400
|
+
(shownOptions as TQorusForm)[name]?.op,
|
|
2401
|
+
isRendererOnly
|
|
2402
|
+
),
|
|
2242
2403
|
(shownOptions as TQorusForm)[name]?.value
|
|
2243
2404
|
);
|
|
2244
2405
|
const comparator = (a: { name: string }, b: { name: string }): number => {
|
|
@@ -2347,7 +2508,7 @@ export const FormEngine = ({
|
|
|
2347
2508
|
optionField={
|
|
2348
2509
|
entry.hidden ?
|
|
2349
2510
|
({
|
|
2350
|
-
type: (options?.[entry.name]
|
|
2511
|
+
type: getOptionSchemaStorageType(options?.[entry.name], isRendererOnly),
|
|
2351
2512
|
value: undefined,
|
|
2352
2513
|
} as IQorusFormField)
|
|
2353
2514
|
: ((shownOptions as TQorusForm)[entry.name] as IQorusFormField)
|
|
@@ -2808,13 +2969,11 @@ export const FormEngine = ({
|
|
|
2808
2969
|
// option's schema as context). The consumer (the IDE) injects
|
|
2809
2970
|
// it; same factory pattern as the ExpressionBuilder's
|
|
2810
2971
|
// `extraActions`.
|
|
2811
|
-
...(
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
})
|
|
2817
|
-
: (optionActions ?? [])),
|
|
2972
|
+
...resolveOptionActions(optionActions, {
|
|
2973
|
+
name: optionName,
|
|
2974
|
+
schema: options[optionName],
|
|
2975
|
+
value: availableOptions?.[optionName] as TOption,
|
|
2976
|
+
}),
|
|
2818
2977
|
{
|
|
2819
2978
|
size: 'tiny',
|
|
2820
2979
|
icon: 'FullscreenLine',
|