@qoretechnologies/reqraft 0.10.16 → 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 +37 -11
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +174 -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 +1 -1
- package/src/components/form/engine/CompactRow.tsx +139 -0
- package/src/components/form/engine/FormEngine.tsx +222 -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
|
|
@@ -644,12 +720,27 @@ export const FormEngine = ({
|
|
|
644
720
|
optionsLoader,
|
|
645
721
|
onValidityChange,
|
|
646
722
|
optionActions,
|
|
723
|
+
optionActionsCollapse = 'auto',
|
|
647
724
|
componentOverrides,
|
|
725
|
+
rendererOnlyUiTypes,
|
|
648
726
|
inheritedFromParent,
|
|
649
727
|
autoFocusFirstRequired,
|
|
650
728
|
initialExpandedOptions,
|
|
651
729
|
...rest
|
|
652
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;
|
|
653
744
|
const [options, setOptions] = useState<IQorusFormSchema | undefined>(rest?.options || undefined);
|
|
654
745
|
// optionsLoader lifecycle: loading feeds the skeleton gate, error the banner.
|
|
655
746
|
const [optionsLoading, setOptionsLoading] = useState<boolean>(!!optionsLoader && !rest?.options);
|
|
@@ -785,7 +876,7 @@ export const FormEngine = ({
|
|
|
785
876
|
fields: TQorusForm | TQorusFlatForm;
|
|
786
877
|
meta?: IOptionsOnChangeMeta;
|
|
787
878
|
}>(() => ({
|
|
788
|
-
fields: fixOptions(value, options || {}),
|
|
879
|
+
fields: fixOptions(value, options || {}, undefined, isRendererOnly),
|
|
789
880
|
meta: undefined,
|
|
790
881
|
}));
|
|
791
882
|
const originalValue = useRef<any>();
|
|
@@ -878,7 +969,7 @@ export const FormEngine = ({
|
|
|
878
969
|
setOptions({});
|
|
879
970
|
return;
|
|
880
971
|
}
|
|
881
|
-
setLocalValue({ fields: fixOptions(value, data.data), meta: undefined });
|
|
972
|
+
setLocalValue({ fields: fixOptions(value, data.data, undefined, isRendererOnly), meta: undefined });
|
|
882
973
|
if (!operatorsUrl) {
|
|
883
974
|
setLoading(false);
|
|
884
975
|
}
|
|
@@ -923,7 +1014,7 @@ export const FormEngine = ({
|
|
|
923
1014
|
}
|
|
924
1015
|
setOptions(data.data);
|
|
925
1016
|
onOptionsLoaded?.(data.data);
|
|
926
|
-
setLocalValue({ fields: fixOptions({}, data.data), meta: undefined });
|
|
1017
|
+
setLocalValue({ fields: fixOptions({}, data.data, undefined, isRendererOnly), meta: undefined });
|
|
927
1018
|
})();
|
|
928
1019
|
}
|
|
929
1020
|
}, [url, customUrl]);
|
|
@@ -947,7 +1038,7 @@ export const FormEngine = ({
|
|
|
947
1038
|
}, [operatorsUrl]);
|
|
948
1039
|
|
|
949
1040
|
useUpdateEffect(() => {
|
|
950
|
-
const fixedValue = fixOptions(value, options || {});
|
|
1041
|
+
const fixedValue = fixOptions(value, options || {}, undefined, isRendererOnly);
|
|
951
1042
|
|
|
952
1043
|
// When the value we're receiving is the one we just emitted AND fixOptions has nothing to
|
|
953
1044
|
// meaningfully add or change, skip the update. This breaks the controlled-component loop for
|
|
@@ -971,19 +1062,21 @@ export const FormEngine = ({
|
|
|
971
1062
|
}
|
|
972
1063
|
|
|
973
1064
|
setLocalValue?.({ fields: fixedValue, meta: undefined });
|
|
974
|
-
}, [JSON.stringify(options), JSON.stringify(value)]);
|
|
1065
|
+
}, [JSON.stringify(options), JSON.stringify(value), isRendererOnly]);
|
|
975
1066
|
|
|
976
1067
|
const handleValueChange = useCallback(
|
|
977
1068
|
(optionName: string, val?: any, _type?: string, isFunction?: boolean) => {
|
|
978
1069
|
setLocalValue(({ fields = {} }) => {
|
|
979
|
-
const schemaType = (options?.[optionName]
|
|
980
|
-
options?.[optionName]?.type) as TQorusType;
|
|
1070
|
+
const schemaType = getOptionSchemaStorageType(options?.[optionName], isRendererOnly);
|
|
981
1071
|
const isAnyLike = schemaType === 'any' || schemaType === 'auto';
|
|
982
1072
|
// For any/auto schema types, preserve the user's chosen type stored in the field
|
|
983
1073
|
const resolvedSchemaType =
|
|
984
|
-
isAnyLike && (fields[optionName] as IQorusFormField)?.type
|
|
985
|
-
((fields[optionName] as IQorusFormField).type as TQorusType)
|
|
986
|
-
|
|
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);
|
|
987
1080
|
const type =
|
|
988
1081
|
_type ||
|
|
989
1082
|
getTypeAndCanBeNull(resolvedSchemaType, options?.[optionName]?.allowed_values).type;
|
|
@@ -1066,6 +1159,7 @@ export const FormEngine = ({
|
|
|
1066
1159
|
[
|
|
1067
1160
|
onSingleOptionsChange,
|
|
1068
1161
|
onDependableOptionChange,
|
|
1162
|
+
isRendererOnly,
|
|
1069
1163
|
JSON.stringify(options),
|
|
1070
1164
|
JSON.stringify(operators),
|
|
1071
1165
|
]
|
|
@@ -1186,8 +1280,7 @@ export const FormEngine = ({
|
|
|
1186
1280
|
optionName as string,
|
|
1187
1281
|
getDefaultValue(options?.[optionName as string]),
|
|
1188
1282
|
getTypeAndCanBeNull(
|
|
1189
|
-
(options?.[optionName as string]
|
|
1190
|
-
options?.[optionName as string]?.type) as TQorusType,
|
|
1283
|
+
getOptionSchemaStorageType(options?.[optionName as string], isRendererOnly),
|
|
1191
1284
|
options?.[optionName as string]?.allowed_values
|
|
1192
1285
|
).type
|
|
1193
1286
|
);
|
|
@@ -1215,36 +1308,37 @@ export const FormEngine = ({
|
|
|
1215
1308
|
return newValue;
|
|
1216
1309
|
}
|
|
1217
1310
|
|
|
1218
|
-
const
|
|
1311
|
+
const rendererType = getType(
|
|
1219
1312
|
(options[optionName].ui_type || options[optionName].type) as TQorusType,
|
|
1220
1313
|
operators,
|
|
1221
1314
|
(option as IQorusFormField)?.op
|
|
1222
1315
|
);
|
|
1316
|
+
const storageType = getOptionFieldStorageType(
|
|
1317
|
+
optionName,
|
|
1318
|
+
(option as IQorusFormField)?.type,
|
|
1319
|
+
options,
|
|
1320
|
+
operators,
|
|
1321
|
+
(option as IQorusFormField)?.op,
|
|
1322
|
+
isRendererOnly
|
|
1323
|
+
);
|
|
1223
1324
|
|
|
1224
1325
|
if (!isPlainObject(option)) {
|
|
1225
1326
|
return {
|
|
1226
1327
|
...newValue,
|
|
1227
1328
|
[optionName]: {
|
|
1228
|
-
type:
|
|
1329
|
+
type: storageType || rendererType,
|
|
1229
1330
|
value: option,
|
|
1230
1331
|
},
|
|
1231
1332
|
};
|
|
1232
1333
|
}
|
|
1233
1334
|
|
|
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
1335
|
return {
|
|
1243
1336
|
...newValue,
|
|
1244
|
-
[optionName]: { ...(option as IQorusFormField), type:
|
|
1337
|
+
[optionName]: { ...(option as IQorusFormField), type: storageType || rendererType },
|
|
1245
1338
|
};
|
|
1246
1339
|
}, {});
|
|
1247
1340
|
}, [
|
|
1341
|
+
isRendererOnly,
|
|
1248
1342
|
JSON.stringify(fixedValue),
|
|
1249
1343
|
JSON.stringify(options),
|
|
1250
1344
|
unavailableOptionsCount.current,
|
|
@@ -1341,10 +1435,14 @@ export const FormEngine = ({
|
|
|
1341
1435
|
const fields: IFormFieldValidityData[] = reduce(
|
|
1342
1436
|
availableOptions,
|
|
1343
1437
|
(result, option, optionName) => {
|
|
1344
|
-
const type =
|
|
1345
|
-
|
|
1346
|
-
(
|
|
1347
|
-
|
|
1438
|
+
const type = getOptionFieldStorageType(
|
|
1439
|
+
optionName,
|
|
1440
|
+
(option as IQorusFormField).type,
|
|
1441
|
+
options,
|
|
1442
|
+
operators,
|
|
1443
|
+
(option as IQorusFormField).op,
|
|
1444
|
+
isRendererOnly
|
|
1445
|
+
);
|
|
1348
1446
|
const optionValue = (option as IQorusFormField).value;
|
|
1349
1447
|
|
|
1350
1448
|
const isRequired = options?.[optionName]?.required;
|
|
@@ -1386,8 +1484,10 @@ export const FormEngine = ({
|
|
|
1386
1484
|
invalidFields,
|
|
1387
1485
|
};
|
|
1388
1486
|
}, [
|
|
1487
|
+
isRendererOnly,
|
|
1389
1488
|
JSON.stringify(availableOptions),
|
|
1390
1489
|
JSON.stringify(options),
|
|
1490
|
+
JSON.stringify(operators),
|
|
1391
1491
|
JSON.stringify(localValue.fields),
|
|
1392
1492
|
]);
|
|
1393
1493
|
|
|
@@ -1406,8 +1506,14 @@ export const FormEngine = ({
|
|
|
1406
1506
|
showInvalidOptionsOnly &&
|
|
1407
1507
|
isOptionValid(
|
|
1408
1508
|
optionName,
|
|
1409
|
-
(
|
|
1410
|
-
|
|
1509
|
+
getOptionFieldStorageType(
|
|
1510
|
+
optionName,
|
|
1511
|
+
(option as IQorusFormField).type,
|
|
1512
|
+
options,
|
|
1513
|
+
operators,
|
|
1514
|
+
(option as IQorusFormField).op,
|
|
1515
|
+
isRendererOnly
|
|
1516
|
+
),
|
|
1411
1517
|
(option as IQorusFormField).value
|
|
1412
1518
|
)
|
|
1413
1519
|
) {
|
|
@@ -1417,7 +1523,13 @@ export const FormEngine = ({
|
|
|
1417
1523
|
},
|
|
1418
1524
|
{}
|
|
1419
1525
|
);
|
|
1420
|
-
}, [
|
|
1526
|
+
}, [
|
|
1527
|
+
showInvalidOptionsOnly,
|
|
1528
|
+
isRendererOnly,
|
|
1529
|
+
JSON.stringify(availableOptions),
|
|
1530
|
+
JSON.stringify(options),
|
|
1531
|
+
JSON.stringify(operators),
|
|
1532
|
+
]);
|
|
1421
1533
|
|
|
1422
1534
|
// Read-first STATUS / BOX for one option — lifted to component scope so the
|
|
1423
1535
|
// status boxes (renderCompact) and the header's "needs attention" count share
|
|
@@ -1437,7 +1549,14 @@ export const FormEngine = ({
|
|
|
1437
1549
|
(name: string, hidden = false): TReadFirstStatus => {
|
|
1438
1550
|
if (hidden) return 'optional';
|
|
1439
1551
|
const schema = options?.[name];
|
|
1440
|
-
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
|
+
);
|
|
1441
1560
|
const value = (availableOptions as TQorusForm)?.[name]?.value;
|
|
1442
1561
|
const empty = isOptionValueEmpty(value);
|
|
1443
1562
|
const reqGroups = (schema?.required_groups as string[] | undefined) || [];
|
|
@@ -1459,8 +1578,10 @@ export const FormEngine = ({
|
|
|
1459
1578
|
});
|
|
1460
1579
|
},
|
|
1461
1580
|
[
|
|
1581
|
+
isRendererOnly,
|
|
1462
1582
|
JSON.stringify(options),
|
|
1463
1583
|
JSON.stringify(availableOptions),
|
|
1584
|
+
JSON.stringify(operators),
|
|
1464
1585
|
isOptionValid,
|
|
1465
1586
|
requiredGroupsInfo,
|
|
1466
1587
|
schemaMsgIntent,
|
|
@@ -1726,10 +1847,10 @@ export const FormEngine = ({
|
|
|
1726
1847
|
next[optionName] = value as IQorusFormField;
|
|
1727
1848
|
}
|
|
1728
1849
|
});
|
|
1729
|
-
return { fields: fixOptions(next, options || {}, operators), meta: undefined };
|
|
1850
|
+
return { fields: fixOptions(next, options || {}, operators, isRendererOnly), meta: undefined };
|
|
1730
1851
|
});
|
|
1731
1852
|
setRequiredOnly(false);
|
|
1732
|
-
}, [JSON.stringify(options), JSON.stringify(operators)]);
|
|
1853
|
+
}, [JSON.stringify(options), JSON.stringify(operators), isRendererOnly]);
|
|
1733
1854
|
|
|
1734
1855
|
const getCustomMenuTemplateItems = useCallback<(optionName: string) => TCustomTemplateItems>(
|
|
1735
1856
|
(optionName) => {
|
|
@@ -1759,6 +1880,25 @@ export const FormEngine = ({
|
|
|
1759
1880
|
suppressSchemaMessages?: boolean
|
|
1760
1881
|
) => {
|
|
1761
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';
|
|
1762
1902
|
return (
|
|
1763
1903
|
<>
|
|
1764
1904
|
{(() => {
|
|
@@ -1900,15 +2040,18 @@ export const FormEngine = ({
|
|
|
1900
2040
|
// (DPQL text mode); opt out per-form via `templateFieldProps`.
|
|
1901
2041
|
allowTextExpressions
|
|
1902
2042
|
allowCustomValues={
|
|
1903
|
-
options?.[optionName]?.supports_custom_values !== false &&
|
|
2043
|
+
options?.[optionName]?.supports_custom_values !== false && resolvedType !== 'any'
|
|
1904
2044
|
}
|
|
1905
2045
|
templates={templates.value}
|
|
1906
2046
|
{...getTypeAndCanBeNull(
|
|
1907
|
-
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,
|
|
1908
2051
|
options?.[optionName]?.allowed_values,
|
|
1909
2052
|
other.op
|
|
1910
2053
|
)}
|
|
1911
|
-
ui_type={
|
|
2054
|
+
ui_type={resolvedType}
|
|
1912
2055
|
name={optionName}
|
|
1913
2056
|
uniqueName={`${uniqueName ? `${uniqueName}.` : `${name ? `${name}.` : ''}`}${optionName}`}
|
|
1914
2057
|
onChange={
|
|
@@ -1949,7 +2092,7 @@ export const FormEngine = ({
|
|
|
1949
2092
|
schema={options || {}}
|
|
1950
2093
|
allOptions={availableOptions}
|
|
1951
2094
|
name={optionName}
|
|
1952
|
-
option={{ type, ...other }}
|
|
2095
|
+
option={{ type: resolvedType, ...other }}
|
|
1953
2096
|
getType={getTypeForOption}
|
|
1954
2097
|
/>
|
|
1955
2098
|
{operators && size(operators) && size(other.op) ?
|
|
@@ -1964,7 +2107,7 @@ export const FormEngine = ({
|
|
|
1964
2107
|
intent='info'
|
|
1965
2108
|
label={
|
|
1966
2109
|
other.value ?
|
|
1967
|
-
|
|
2110
|
+
resolvedType === 'richtext' ?
|
|
1968
2111
|
richtextToString(other.value)
|
|
1969
2112
|
: JSON.stringify(other.value)
|
|
1970
2113
|
: ''
|
|
@@ -2057,6 +2200,8 @@ export const FormEngine = ({
|
|
|
2057
2200
|
getTypeForOption,
|
|
2058
2201
|
isOptionValid,
|
|
2059
2202
|
confirmAction,
|
|
2203
|
+
optionActions,
|
|
2204
|
+
collapseOptionActions,
|
|
2060
2205
|
renderOption,
|
|
2061
2206
|
theme,
|
|
2062
2207
|
cText,
|
|
@@ -2102,6 +2247,8 @@ export const FormEngine = ({
|
|
|
2102
2247
|
getTypeForOption,
|
|
2103
2248
|
isOptionValid,
|
|
2104
2249
|
confirmAction,
|
|
2250
|
+
optionActions,
|
|
2251
|
+
collapseOptionActions,
|
|
2105
2252
|
renderOption,
|
|
2106
2253
|
theme,
|
|
2107
2254
|
cText,
|
|
@@ -2238,7 +2385,14 @@ export const FormEngine = ({
|
|
|
2238
2385
|
const isFieldInvalid = (name: string) =>
|
|
2239
2386
|
!isOptionValid(
|
|
2240
2387
|
name,
|
|
2241
|
-
(
|
|
2388
|
+
getOptionFieldStorageType(
|
|
2389
|
+
name,
|
|
2390
|
+
(shownOptions as TQorusForm)[name]?.type,
|
|
2391
|
+
options,
|
|
2392
|
+
operators,
|
|
2393
|
+
(shownOptions as TQorusForm)[name]?.op,
|
|
2394
|
+
isRendererOnly
|
|
2395
|
+
),
|
|
2242
2396
|
(shownOptions as TQorusForm)[name]?.value
|
|
2243
2397
|
);
|
|
2244
2398
|
const comparator = (a: { name: string }, b: { name: string }): number => {
|
|
@@ -2347,7 +2501,7 @@ export const FormEngine = ({
|
|
|
2347
2501
|
optionField={
|
|
2348
2502
|
entry.hidden ?
|
|
2349
2503
|
({
|
|
2350
|
-
type: (options?.[entry.name]
|
|
2504
|
+
type: getOptionSchemaStorageType(options?.[entry.name], isRendererOnly),
|
|
2351
2505
|
value: undefined,
|
|
2352
2506
|
} as IQorusFormField)
|
|
2353
2507
|
: ((shownOptions as TQorusForm)[entry.name] as IQorusFormField)
|
|
@@ -2808,13 +2962,11 @@ export const FormEngine = ({
|
|
|
2808
2962
|
// option's schema as context). The consumer (the IDE) injects
|
|
2809
2963
|
// it; same factory pattern as the ExpressionBuilder's
|
|
2810
2964
|
// `extraActions`.
|
|
2811
|
-
...(
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
})
|
|
2817
|
-
: (optionActions ?? [])),
|
|
2965
|
+
...resolveOptionActions(optionActions, {
|
|
2966
|
+
name: optionName,
|
|
2967
|
+
schema: options[optionName],
|
|
2968
|
+
value: availableOptions?.[optionName] as TOption,
|
|
2969
|
+
}),
|
|
2818
2970
|
{
|
|
2819
2971
|
size: 'tiny',
|
|
2820
2972
|
icon: 'FullscreenLine',
|