@nocobase/client-v2 2.2.0-beta.14 → 2.2.0-beta.16
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/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -2
- package/es/components/form/TypedVariableInput.d.ts +31 -5
- package/es/components/form/filter/CollectionFilter.d.ts +2 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +3 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/RunJSValueEditor.d.ts +1 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/index.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.d.ts +4 -0
- package/es/index.mjs +143 -106
- package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
- package/es/utils/index.d.ts +1 -0
- package/lib/index.js +154 -117
- package/lib/locale/languageCodes.js +3 -1
- package/package.json +8 -7
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +4 -0
- package/src/components/form/ScanInput/CodeScanner.tsx +61 -34
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +114 -18
- package/src/components/form/ScanInput/useCodeScanner.ts +166 -23
- package/src/components/form/TypedVariableInput.tsx +452 -101
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
- package/src/components/form/filter/CollectionFilter.tsx +4 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/filter/useFilterActionProps.ts +37 -6
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +111 -16
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +76 -9
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/index.ts +3 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +3 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/index.ts +4 -0
- package/src/locale/languageCodes.ts +3 -1
- package/src/nocobase-buildin-plugin/index.tsx +2 -0
- package/src/utils/getRouteRuntimeVersion.ts +185 -0
- package/src/utils/index.tsx +1 -0
|
@@ -42,6 +42,12 @@ import { normalizeFilterValueByOperator } from '../models/blocks/filter-form/val
|
|
|
42
42
|
import { FieldAssignExactDatePicker, type ExactDatePickerMode } from './FieldAssignExactDatePicker';
|
|
43
43
|
import { limitAssociationMetaTree } from './filter/metaTreeAssociationDepth';
|
|
44
44
|
|
|
45
|
+
type VariableInputConverters = {
|
|
46
|
+
renderInputComponent?: (metaTreeNode: MetaTreeNode | null) => React.ComponentType<any> | null;
|
|
47
|
+
resolvePathFromValue?: (value: any) => string[] | undefined;
|
|
48
|
+
resolveValueFromPath?: (metaTreeNode: MetaTreeNode) => any;
|
|
49
|
+
};
|
|
50
|
+
|
|
45
51
|
const DATE_FIELD_INTERFACES = new Set(['date', 'datetime', 'datetimeNoTz', 'createdAt', 'updatedAt', 'unixTimestamp']);
|
|
46
52
|
|
|
47
53
|
const TZ_AWARE_DATE_INTERFACES = new Set(['datetime', 'createdAt', 'updatedAt', 'unixTimestamp']);
|
|
@@ -341,7 +347,11 @@ interface Props {
|
|
|
341
347
|
* 默认 false,保持历史行为。
|
|
342
348
|
*/
|
|
343
349
|
enableDateVariableAsConstant?: boolean;
|
|
350
|
+
/** 是否允许在变量选择器中使用 RunJS。默认 true,保持历史行为。 */
|
|
351
|
+
allowRunJS?: boolean;
|
|
344
352
|
maxAssociationFieldDepth?: number;
|
|
353
|
+
disabled?: boolean;
|
|
354
|
+
variableConverters?: VariableInputConverters;
|
|
345
355
|
}
|
|
346
356
|
|
|
347
357
|
type ResolvedFieldContext = {
|
|
@@ -712,7 +722,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
712
722
|
preferFormItemFieldModel,
|
|
713
723
|
associationFieldNamesOverride,
|
|
714
724
|
enableDateVariableAsConstant = false,
|
|
725
|
+
allowRunJS = true,
|
|
715
726
|
maxAssociationFieldDepth = 2,
|
|
727
|
+
disabled = false,
|
|
728
|
+
variableConverters,
|
|
716
729
|
}) => {
|
|
717
730
|
const flowCtx = useFlowContext<FlowModelContext>();
|
|
718
731
|
const normalizeEventValue = React.useCallback((eventOrValue: unknown) => {
|
|
@@ -1077,7 +1090,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1077
1090
|
fm.setStepParams('selectSettings', 'fieldNames', { label: overrideLabel });
|
|
1078
1091
|
}
|
|
1079
1092
|
fm?.setProps?.({
|
|
1080
|
-
disabled
|
|
1093
|
+
disabled,
|
|
1081
1094
|
readPretty: false,
|
|
1082
1095
|
pattern: 'editable',
|
|
1083
1096
|
updateAssociation: false,
|
|
@@ -1140,6 +1153,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1140
1153
|
preferFormItemFieldModel,
|
|
1141
1154
|
associationFieldNamesOverride?.label,
|
|
1142
1155
|
associationFieldNamesOverride?.value,
|
|
1156
|
+
disabled,
|
|
1143
1157
|
]);
|
|
1144
1158
|
|
|
1145
1159
|
// 当传入 operator / operatorMetaList 时,按 operator schema 适配临时字段的输入组件与 props。
|
|
@@ -1192,6 +1206,9 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1192
1206
|
React.useEffect(() => {
|
|
1193
1207
|
const coercedValue = coerceEmptyValueForRenderer(inputProps?.value);
|
|
1194
1208
|
const handleChange = (ev: any) => {
|
|
1209
|
+
if (inputProps?.disabled) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1195
1212
|
const nextRaw = normalizeEventValue(ev);
|
|
1196
1213
|
const normalizedForStore = operator ? normalizeFilterValueByOperator(operator, nextRaw) : nextRaw;
|
|
1197
1214
|
const nextValue = coerceEmptyValueForRenderer(normalizedForStore);
|
|
@@ -1223,6 +1240,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1223
1240
|
value={inputProps?.value}
|
|
1224
1241
|
onChange={(e) => inputProps?.onChange?.(normalizeEventValue(e))}
|
|
1225
1242
|
placeholder={placeholder}
|
|
1243
|
+
disabled={inputProps?.disabled}
|
|
1226
1244
|
style={withFullWidthStyle(wrapperStyle)}
|
|
1227
1245
|
/>
|
|
1228
1246
|
);
|
|
@@ -1241,6 +1259,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1241
1259
|
const C: React.FC<any> = (inputProps) => {
|
|
1242
1260
|
const wrapperStyle = pickStyle(inputProps?.style);
|
|
1243
1261
|
const raw = inputProps?.value;
|
|
1262
|
+
const isDisabled = Boolean(inputProps?.disabled);
|
|
1244
1263
|
const parsed = isCtxDateExpression(raw) ? parseCtxDateExpression(raw) : raw;
|
|
1245
1264
|
const parsedValue = typeof parsed === 'undefined' ? undefined : parsed;
|
|
1246
1265
|
const { token } = theme.useToken();
|
|
@@ -1265,6 +1284,9 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1265
1284
|
});
|
|
1266
1285
|
|
|
1267
1286
|
const handleSelect = (val: string) => {
|
|
1287
|
+
if (isDisabled) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1268
1290
|
setOpen(false);
|
|
1269
1291
|
if (val === 'exact') {
|
|
1270
1292
|
inputProps?.onChange?.('');
|
|
@@ -1279,10 +1301,16 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1279
1301
|
};
|
|
1280
1302
|
|
|
1281
1303
|
const handleExactSingleChange = (nextValue: any) => {
|
|
1304
|
+
if (isDisabled) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1282
1307
|
inputProps?.onChange?.(nextValue || '');
|
|
1283
1308
|
};
|
|
1284
1309
|
|
|
1285
1310
|
const handleExactRangeChange = (nextValue: any) => {
|
|
1311
|
+
if (isDisabled) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1286
1314
|
inputProps?.onChange?.(nextValue || '');
|
|
1287
1315
|
};
|
|
1288
1316
|
|
|
@@ -1327,7 +1355,11 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1327
1355
|
<Select
|
|
1328
1356
|
options={options}
|
|
1329
1357
|
open={open}
|
|
1330
|
-
onDropdownVisibleChange={
|
|
1358
|
+
onDropdownVisibleChange={(nextOpen) => {
|
|
1359
|
+
if (!isDisabled) {
|
|
1360
|
+
setOpen(nextOpen);
|
|
1361
|
+
}
|
|
1362
|
+
}}
|
|
1331
1363
|
allowClear={false}
|
|
1332
1364
|
style={{
|
|
1333
1365
|
width: '100%',
|
|
@@ -1337,13 +1369,18 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1337
1369
|
value={selectedType}
|
|
1338
1370
|
onChange={handleSelect}
|
|
1339
1371
|
dropdownRender={dropdownRender}
|
|
1372
|
+
disabled={isDisabled}
|
|
1340
1373
|
/>
|
|
1341
1374
|
{['past', 'next'].includes(selectedType) && [
|
|
1342
1375
|
<InputNumber
|
|
1343
1376
|
key="number"
|
|
1344
1377
|
style={{ flex: 1 }}
|
|
1345
1378
|
value={(parsedValue as any)?.number}
|
|
1379
|
+
disabled={isDisabled}
|
|
1346
1380
|
onChange={(nextNumber) => {
|
|
1381
|
+
if (isDisabled) {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1347
1384
|
inputProps?.onChange?.({
|
|
1348
1385
|
...(parsedValue as any),
|
|
1349
1386
|
type: selectedType,
|
|
@@ -1356,7 +1393,11 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1356
1393
|
key="unit"
|
|
1357
1394
|
value={(parsedValue as any)?.unit}
|
|
1358
1395
|
style={{ minWidth: 130, maxWidth: 140 }}
|
|
1396
|
+
disabled={isDisabled}
|
|
1359
1397
|
onChange={(nextUnit) => {
|
|
1398
|
+
if (isDisabled) {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1360
1401
|
inputProps?.onChange?.({
|
|
1361
1402
|
...(parsedValue as any),
|
|
1362
1403
|
type: selectedType,
|
|
@@ -1379,6 +1420,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1379
1420
|
isRange={isRange}
|
|
1380
1421
|
value={isRange ? exactRangeValue : exactSingleValue}
|
|
1381
1422
|
onChange={isRange ? handleExactRangeChange : handleExactSingleChange}
|
|
1423
|
+
disabled={isDisabled}
|
|
1382
1424
|
style={{ flex: 1 }}
|
|
1383
1425
|
/>
|
|
1384
1426
|
)}
|
|
@@ -1398,7 +1440,12 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1398
1440
|
|
|
1399
1441
|
const RunJSComponent = React.useMemo(() => {
|
|
1400
1442
|
const C: React.FC<any> = (inputProps) => (
|
|
1401
|
-
<RunJSValueEditor
|
|
1443
|
+
<RunJSValueEditor
|
|
1444
|
+
t={flowCtx.t}
|
|
1445
|
+
value={inputProps?.value}
|
|
1446
|
+
onChange={inputProps?.onChange}
|
|
1447
|
+
disabled={inputProps?.disabled}
|
|
1448
|
+
/>
|
|
1402
1449
|
);
|
|
1403
1450
|
return C;
|
|
1404
1451
|
}, [flowCtx]);
|
|
@@ -1421,11 +1468,13 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1421
1468
|
render: ConstantEditor,
|
|
1422
1469
|
},
|
|
1423
1470
|
{ title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
1424
|
-
|
|
1471
|
+
...(allowRunJS
|
|
1472
|
+
? [{ title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent }]
|
|
1473
|
+
: []),
|
|
1425
1474
|
...limitedBase,
|
|
1426
1475
|
];
|
|
1427
1476
|
};
|
|
1428
|
-
}, [flowCtx, ConstantEditor, NullComponent, RunJSComponent, maxAssociationFieldDepth]);
|
|
1477
|
+
}, [flowCtx, ConstantEditor, NullComponent, RunJSComponent, allowRunJS, maxAssociationFieldDepth]);
|
|
1429
1478
|
|
|
1430
1479
|
const displayValue = React.useMemo(() => {
|
|
1431
1480
|
if (!useDateVariableConstant) {
|
|
@@ -1442,6 +1491,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1442
1491
|
|
|
1443
1492
|
const handleVariableInputChange = React.useCallback(
|
|
1444
1493
|
(nextValue: any) => {
|
|
1494
|
+
if (disabled) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1445
1498
|
if (!useDateVariableConstant) {
|
|
1446
1499
|
onChange(nextValue);
|
|
1447
1500
|
return;
|
|
@@ -1449,7 +1502,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1449
1502
|
|
|
1450
1503
|
onChange(normalizeDateVariableOutput(nextValue, dateVariableComponentProps));
|
|
1451
1504
|
},
|
|
1452
|
-
[dateVariableComponentProps, onChange, useDateVariableConstant],
|
|
1505
|
+
[dateVariableComponentProps, disabled, onChange, useDateVariableConstant],
|
|
1453
1506
|
);
|
|
1454
1507
|
|
|
1455
1508
|
if (!fieldPath) {
|
|
@@ -1464,26 +1517,40 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1464
1517
|
metaTree={metaTree}
|
|
1465
1518
|
style={{ width: '100%' }}
|
|
1466
1519
|
clearValue={''}
|
|
1520
|
+
disabled={disabled}
|
|
1467
1521
|
converters={{
|
|
1522
|
+
...variableConverters,
|
|
1468
1523
|
renderInputComponent: (meta) => {
|
|
1524
|
+
const external = variableConverters?.renderInputComponent?.(meta ?? null);
|
|
1525
|
+
if (external) {
|
|
1526
|
+
return external;
|
|
1527
|
+
}
|
|
1469
1528
|
const firstPath = meta?.paths?.[0];
|
|
1470
1529
|
if (firstPath === 'constant') return ConstantEditor;
|
|
1471
1530
|
if (firstPath === 'null') return NullComponent;
|
|
1472
|
-
if (firstPath === 'runjs') return RunJSComponent;
|
|
1531
|
+
if (allowRunJS && firstPath === 'runjs') return RunJSComponent;
|
|
1473
1532
|
return null;
|
|
1474
1533
|
},
|
|
1475
1534
|
resolveValueFromPath: (item) => {
|
|
1535
|
+
const external = variableConverters?.resolveValueFromPath?.(item);
|
|
1536
|
+
if (external !== undefined) {
|
|
1537
|
+
return external;
|
|
1538
|
+
}
|
|
1476
1539
|
const firstPath = item?.paths?.[0];
|
|
1477
1540
|
if (firstPath === 'constant') {
|
|
1478
1541
|
return useDateVariableConstant ? { type: 'today' } : '';
|
|
1479
1542
|
}
|
|
1480
1543
|
if (firstPath === 'null') return null;
|
|
1481
|
-
if (firstPath === 'runjs') return { code: '', version: 'v2' };
|
|
1544
|
+
if (allowRunJS && firstPath === 'runjs') return { code: '', version: 'v2' };
|
|
1482
1545
|
return undefined;
|
|
1483
1546
|
},
|
|
1484
1547
|
resolvePathFromValue: (currentValue) => {
|
|
1548
|
+
const external = variableConverters?.resolvePathFromValue?.(currentValue);
|
|
1549
|
+
if (external !== undefined) {
|
|
1550
|
+
return external;
|
|
1551
|
+
}
|
|
1485
1552
|
if (currentValue === null) return ['null'];
|
|
1486
|
-
if (isRunJSValue(currentValue)) return ['runjs'];
|
|
1553
|
+
if (allowRunJS && isRunJSValue(currentValue)) return ['runjs'];
|
|
1487
1554
|
if (useDateVariableConstant && isCtxDateExpression(currentValue)) {
|
|
1488
1555
|
return ['constant'];
|
|
1489
1556
|
}
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { Button, Result } from 'antd';
|
|
11
12
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
-
import {
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
13
14
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
15
|
+
import { getModernClientPrefix, stripModernClientPrefix } from '../../authRedirect';
|
|
14
16
|
import { useApp } from '../../hooks/useApp';
|
|
15
17
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
|
|
16
18
|
import {
|
|
@@ -22,12 +24,14 @@ import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutM
|
|
|
22
24
|
import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
|
|
23
25
|
import { AppNotFound } from '../../components';
|
|
24
26
|
import { useKeepAlive } from '../../components/KeepAlive';
|
|
27
|
+
import { registerDeviceTypeContext } from '../internal/registerDeviceTypeContext';
|
|
25
28
|
|
|
26
29
|
type FlowRouteGuardState = {
|
|
27
30
|
pageUid?: string;
|
|
28
31
|
pending: boolean;
|
|
29
32
|
allowBridge: boolean;
|
|
30
33
|
notFound: boolean;
|
|
34
|
+
legacyPageUnsupported?: boolean;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
|
|
@@ -156,31 +160,9 @@ const BridgeFlowRoute = ({
|
|
|
156
160
|
const layoutContentRef = useRef<HTMLDivElement>(null);
|
|
157
161
|
|
|
158
162
|
useEffect(() => {
|
|
159
|
-
flowEngine.context.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
meta: {
|
|
163
|
-
type: 'string',
|
|
164
|
-
title: flowEngine.translate('Current device type'),
|
|
165
|
-
interface: 'select',
|
|
166
|
-
uiSchema: {
|
|
167
|
-
enum: [
|
|
168
|
-
{ label: flowEngine.translate('Computer'), value: 'computer' },
|
|
169
|
-
{ label: flowEngine.translate('Mobile'), value: 'mobile' },
|
|
170
|
-
{ label: flowEngine.translate('Tablet'), value: 'tablet' },
|
|
171
|
-
{ label: flowEngine.translate('SmartTv'), value: 'smarttv' },
|
|
172
|
-
{ label: flowEngine.translate('Console'), value: 'console' },
|
|
173
|
-
{ label: flowEngine.translate('Wearable'), value: 'wearable' },
|
|
174
|
-
{ label: flowEngine.translate('Embedded'), value: 'embedded' },
|
|
175
|
-
],
|
|
176
|
-
'x-component': 'Select',
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
info: {
|
|
180
|
-
description: 'Current device type (computer/mobile/tablet/...).',
|
|
181
|
-
detail: 'string',
|
|
182
|
-
},
|
|
183
|
-
});
|
|
163
|
+
if (!flowEngine.context.getPropertyOptions('deviceType')) {
|
|
164
|
+
registerDeviceTypeContext(flowEngine);
|
|
165
|
+
}
|
|
184
166
|
}, [flowEngine]);
|
|
185
167
|
|
|
186
168
|
useLayoutRoutePage({
|
|
@@ -195,11 +177,58 @@ const BridgeFlowRoute = ({
|
|
|
195
177
|
return <div ref={layoutContentRef} />;
|
|
196
178
|
};
|
|
197
179
|
|
|
180
|
+
type RouteLocation = {
|
|
181
|
+
pathname: string;
|
|
182
|
+
search: string;
|
|
183
|
+
hash: string;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getLegacyPageHref = (app: { getPublicPath: () => string }, location: RouteLocation) => {
|
|
187
|
+
const modernPublicPath = app.getPublicPath();
|
|
188
|
+
const browserLocationMatchesPublicPath = window.location.pathname.startsWith(modernPublicPath);
|
|
189
|
+
const currentLocation = browserLocationMatchesPublicPath ? window.location : location;
|
|
190
|
+
const pathWithinModernClient = currentLocation.pathname.startsWith(modernPublicPath)
|
|
191
|
+
? currentLocation.pathname.slice(modernPublicPath.length)
|
|
192
|
+
: currentLocation.pathname.replace(/^\/+/, '');
|
|
193
|
+
return `${stripModernClientPrefix(modernPublicPath)}${pathWithinModernClient}${currentLocation.search}${
|
|
194
|
+
currentLocation.hash
|
|
195
|
+
}`;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const LegacyPageUnsupported = ({
|
|
199
|
+
app,
|
|
200
|
+
location,
|
|
201
|
+
}: {
|
|
202
|
+
app: { getPublicPath: () => string };
|
|
203
|
+
location: RouteLocation;
|
|
204
|
+
}) => {
|
|
205
|
+
const { t } = useTranslation();
|
|
206
|
+
const modernClientPath = `/${getModernClientPrefix()}/`;
|
|
207
|
+
const withModernClientPath = (message: string) => message.replaceAll('{{modernClientPath}}', modernClientPath);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Result
|
|
211
|
+
status="warning"
|
|
212
|
+
title={withModernClientPath(t('This page is not supported in the {{modernClientPath}} branch'))}
|
|
213
|
+
subTitle={withModernClientPath(
|
|
214
|
+
t(
|
|
215
|
+
'The {{modernClientPath}} branch only supports new pages. This page is a legacy page. Please open it from the original entry.',
|
|
216
|
+
),
|
|
217
|
+
)}
|
|
218
|
+
extra={
|
|
219
|
+
<Button href={getLegacyPageHref(app, location)} type="primary">
|
|
220
|
+
{t('Open from the original entry')}
|
|
221
|
+
</Button>
|
|
222
|
+
}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
};
|
|
226
|
+
|
|
198
227
|
/**
|
|
199
228
|
* 管理后台动态页面路由组件。
|
|
200
229
|
*
|
|
201
|
-
* 负责读取当前路由页面 UID
|
|
202
|
-
*
|
|
230
|
+
* 负责读取当前路由页面 UID,并把页面生命周期桥接到 AdminLayout host model。
|
|
231
|
+
* 设备变量通常由 PluginFlowEngine 共享初始化提供;独立渲染时会在挂载后补充注册。
|
|
203
232
|
*
|
|
204
233
|
* @example
|
|
205
234
|
* ```tsx
|
|
@@ -377,7 +406,13 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
377
406
|
|
|
378
407
|
if (target.reason === 'unsupportedV2Runtime') {
|
|
379
408
|
if (active && requestId === requestIdRef.current) {
|
|
380
|
-
setGuardState({
|
|
409
|
+
setGuardState({
|
|
410
|
+
pageUid,
|
|
411
|
+
pending: false,
|
|
412
|
+
allowBridge: false,
|
|
413
|
+
notFound: false,
|
|
414
|
+
legacyPageUnsupported: true,
|
|
415
|
+
});
|
|
381
416
|
}
|
|
382
417
|
return;
|
|
383
418
|
}
|
|
@@ -414,6 +449,10 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
414
449
|
return <AppNotFound />;
|
|
415
450
|
}
|
|
416
451
|
|
|
452
|
+
if (guardState.legacyPageUnsupported) {
|
|
453
|
+
return <LegacyPageUnsupported app={app} location={location} />;
|
|
454
|
+
}
|
|
455
|
+
|
|
417
456
|
if (!guardState.allowBridge) {
|
|
418
457
|
return null;
|
|
419
458
|
}
|
|
@@ -421,11 +460,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
421
460
|
return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
|
|
422
461
|
}, [
|
|
423
462
|
active,
|
|
463
|
+
app,
|
|
424
464
|
getLayoutModel,
|
|
425
465
|
guardState.allowBridge,
|
|
466
|
+
guardState.legacyPageUnsupported,
|
|
426
467
|
guardState.notFound,
|
|
427
468
|
guardState.pageUid,
|
|
428
469
|
guardState.pending,
|
|
470
|
+
location,
|
|
429
471
|
pageUid,
|
|
430
472
|
]);
|
|
431
473
|
|
|
@@ -15,6 +15,7 @@ export interface RunJSValueEditorProps {
|
|
|
15
15
|
t?: (key: string) => string;
|
|
16
16
|
value?: unknown;
|
|
17
17
|
onChange?: (value: RunJSValue) => void;
|
|
18
|
+
disabled?: boolean;
|
|
18
19
|
height?: string;
|
|
19
20
|
scene?: string;
|
|
20
21
|
containerStyle?: React.CSSProperties;
|
|
@@ -25,6 +26,7 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
|
|
|
25
26
|
t,
|
|
26
27
|
value,
|
|
27
28
|
onChange,
|
|
29
|
+
disabled,
|
|
28
30
|
height = '200px',
|
|
29
31
|
scene = 'formValue',
|
|
30
32
|
containerStyle = { flex: 1, minWidth: 0 },
|
|
@@ -38,9 +40,15 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
|
|
|
38
40
|
<div style={containerStyle}>
|
|
39
41
|
<CodeEditor
|
|
40
42
|
value={current.code}
|
|
41
|
-
onChange={(code) =>
|
|
43
|
+
onChange={(code) => {
|
|
44
|
+
if (disabled) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
onChange?.({ ...current, code });
|
|
48
|
+
}}
|
|
42
49
|
version={current.version}
|
|
43
50
|
height={height}
|
|
51
|
+
readonly={disabled}
|
|
44
52
|
enableLinter
|
|
45
53
|
placeholder={placeholderText}
|
|
46
54
|
scene={scene}
|
|
@@ -12,9 +12,10 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
12
12
|
import { render, waitFor } from '@nocobase/test/client';
|
|
13
13
|
import { FieldAssignValueInput } from '../FieldAssignValueInput';
|
|
14
14
|
|
|
15
|
-
const { mockUseFlowContext, mockGetDefaultBindingByField } = vi.hoisted(() => ({
|
|
15
|
+
const { mockUseFlowContext, mockGetDefaultBindingByField, mockVariableInput } = vi.hoisted(() => ({
|
|
16
16
|
mockUseFlowContext: vi.fn(),
|
|
17
17
|
mockGetDefaultBindingByField: vi.fn(),
|
|
18
|
+
mockVariableInput: vi.fn(() => <div data-testid="variable-input" />),
|
|
18
19
|
}));
|
|
19
20
|
|
|
20
21
|
vi.mock('@nocobase/flow-engine', async () => {
|
|
@@ -25,7 +26,7 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
25
26
|
return {
|
|
26
27
|
...actual,
|
|
27
28
|
useFlowContext: () => mockUseFlowContext(),
|
|
28
|
-
VariableInput:
|
|
29
|
+
VariableInput: mockVariableInput,
|
|
29
30
|
FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
|
|
30
31
|
EditableItemModel: MockEditableItemModel,
|
|
31
32
|
};
|
|
@@ -131,4 +132,85 @@ describe('FieldAssignValueInput context', () => {
|
|
|
131
132
|
});
|
|
132
133
|
expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
|
|
133
134
|
});
|
|
135
|
+
|
|
136
|
+
it('lets callers override variable path parsing for domain-specific stored formats', async () => {
|
|
137
|
+
mockVariableInput.mockClear();
|
|
138
|
+
const sourceContext = {
|
|
139
|
+
dataSourceManager: {
|
|
140
|
+
getDataSource: vi.fn(() => ({})),
|
|
141
|
+
},
|
|
142
|
+
t: (key: string) => key,
|
|
143
|
+
};
|
|
144
|
+
const fieldModel = {
|
|
145
|
+
props: {},
|
|
146
|
+
setProps: vi.fn(),
|
|
147
|
+
dispatchEvent: vi.fn(),
|
|
148
|
+
remove: vi.fn(),
|
|
149
|
+
};
|
|
150
|
+
const tempRoot = {
|
|
151
|
+
context: {
|
|
152
|
+
defineProperty: vi.fn(),
|
|
153
|
+
},
|
|
154
|
+
subModels: {
|
|
155
|
+
fields: [fieldModel],
|
|
156
|
+
},
|
|
157
|
+
setProps: vi.fn(),
|
|
158
|
+
remove: vi.fn(),
|
|
159
|
+
};
|
|
160
|
+
const engine = {
|
|
161
|
+
createModel: vi.fn(() => tempRoot),
|
|
162
|
+
};
|
|
163
|
+
const collectionField = {
|
|
164
|
+
name: 'status',
|
|
165
|
+
interface: 'input',
|
|
166
|
+
uiSchema: { 'x-component': 'Input' },
|
|
167
|
+
isAssociationField: () => false,
|
|
168
|
+
getComponentProps: () => ({}),
|
|
169
|
+
};
|
|
170
|
+
const collection = {
|
|
171
|
+
dataSourceKey: 'main',
|
|
172
|
+
name: 'tasks',
|
|
173
|
+
getField: (name: string) => (name === 'status' ? collectionField : null),
|
|
174
|
+
getFields: () => [collectionField],
|
|
175
|
+
};
|
|
176
|
+
const formModel = {
|
|
177
|
+
context: { ...sourceContext, engine, collection, blockModel: null as any },
|
|
178
|
+
collection,
|
|
179
|
+
subModels: {},
|
|
180
|
+
};
|
|
181
|
+
formModel.context.blockModel = formModel;
|
|
182
|
+
|
|
183
|
+
mockGetDefaultBindingByField.mockReturnValue({ modelName: 'InputFieldModel' });
|
|
184
|
+
mockUseFlowContext.mockReturnValue({
|
|
185
|
+
model: formModel,
|
|
186
|
+
t: (key: string) => key,
|
|
187
|
+
getPropertyMetaTree: vi.fn(async () => []),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const variableConverters = {
|
|
191
|
+
resolvePathFromValue: vi.fn((value: string) =>
|
|
192
|
+
value === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
|
|
193
|
+
),
|
|
194
|
+
resolveValueFromPath: vi.fn(() => undefined),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
render(
|
|
198
|
+
<FieldAssignValueInput
|
|
199
|
+
targetPath="status"
|
|
200
|
+
value="{{$context.data.updatedAt}}"
|
|
201
|
+
onChange={vi.fn()}
|
|
202
|
+
variableConverters={variableConverters}
|
|
203
|
+
/>,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(engine.createModel).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
const latestVariableInputCall = mockVariableInput.mock.calls.at(-1)?.[0];
|
|
210
|
+
expect(latestVariableInputCall.converters.resolvePathFromValue('{{$context.data.updatedAt}}')).toEqual([
|
|
211
|
+
'$context',
|
|
212
|
+
'data',
|
|
213
|
+
'updatedAt',
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
134
216
|
});
|
|
@@ -45,6 +45,7 @@ interface FilterGroupProps {
|
|
|
45
45
|
value: Record<string, any>;
|
|
46
46
|
/** 自定义筛选项组件 */
|
|
47
47
|
FilterItem?: React.FC<FilterItemProps>;
|
|
48
|
+
disabled?: boolean;
|
|
48
49
|
closeIcon?: ReactNode;
|
|
49
50
|
/** 是否显示边框 */
|
|
50
51
|
showBorder?: boolean;
|
|
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
89
90
|
const {
|
|
90
91
|
value = { logic: '$and', items: [] },
|
|
91
92
|
FilterItem,
|
|
93
|
+
disabled = false,
|
|
92
94
|
showBorder = false,
|
|
93
95
|
onRemove,
|
|
94
96
|
onChange,
|
|
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
const handleLogicChange = (newLogic: '$and' | '$or') => {
|
|
125
|
+
if (disabled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
123
128
|
value.logic = newLogic;
|
|
124
129
|
onChange?.(value);
|
|
125
130
|
};
|
|
126
131
|
|
|
127
132
|
const handleAddCondition = () => {
|
|
133
|
+
if (disabled) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
128
136
|
items.push({
|
|
129
137
|
path: '',
|
|
130
138
|
operator: '',
|
|
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
134
142
|
};
|
|
135
143
|
|
|
136
144
|
const handleAddConditionGroup = () => {
|
|
145
|
+
if (disabled) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
137
148
|
items.push({
|
|
138
149
|
logic: '$and',
|
|
139
150
|
items: [],
|
|
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
142
153
|
};
|
|
143
154
|
|
|
144
155
|
const handleRemoveItem = (index: number) => {
|
|
156
|
+
if (disabled) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
145
159
|
items.splice(index, 1);
|
|
146
160
|
onChange?.(value);
|
|
147
161
|
};
|
|
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
160
174
|
<button
|
|
161
175
|
type="button"
|
|
162
176
|
aria-label="icon-close"
|
|
177
|
+
disabled={disabled}
|
|
163
178
|
style={{
|
|
164
179
|
position: 'absolute',
|
|
165
180
|
right: 10,
|
|
166
181
|
top: 10,
|
|
167
|
-
cursor: 'pointer',
|
|
182
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
168
183
|
background: 'transparent',
|
|
169
184
|
border: 0,
|
|
170
185
|
padding: 0,
|
|
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
182
197
|
data-testid="filter-select-all-or-any"
|
|
183
198
|
style={{ width: 'auto' }}
|
|
184
199
|
value={logic}
|
|
200
|
+
disabled={disabled}
|
|
185
201
|
onChange={handleLogicChange}
|
|
186
202
|
>
|
|
187
203
|
<Select.Option value="$and">{t('All')}</Select.Option>
|
|
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
200
216
|
key={getFilterItemKey(item)}
|
|
201
217
|
value={item}
|
|
202
218
|
FilterItem={FilterItem}
|
|
219
|
+
disabled={disabled}
|
|
203
220
|
showBorder={true}
|
|
204
221
|
onRemove={() => handleRemoveItem(index)}
|
|
205
222
|
onChange={(v) => {
|
|
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
221
238
|
<button
|
|
222
239
|
type="button"
|
|
223
240
|
aria-label="icon-close"
|
|
241
|
+
disabled={disabled}
|
|
224
242
|
style={{
|
|
225
243
|
marginLeft: 8,
|
|
226
244
|
marginRight: 8,
|
|
227
245
|
flex: '0 0 auto',
|
|
228
|
-
cursor: 'pointer',
|
|
246
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
229
247
|
background: 'transparent',
|
|
230
248
|
border: 0,
|
|
231
249
|
padding: 0,
|
|
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
251
269
|
<button
|
|
252
270
|
type="button"
|
|
253
271
|
aria-label="icon-close"
|
|
272
|
+
disabled={disabled}
|
|
254
273
|
style={{
|
|
255
274
|
marginLeft: 8,
|
|
256
275
|
marginRight: 8,
|
|
257
276
|
flex: '0 0 auto',
|
|
258
|
-
cursor: 'pointer',
|
|
277
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
259
278
|
background: 'transparent',
|
|
260
279
|
border: 0,
|
|
261
280
|
padding: 0,
|
|
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
277
296
|
<button
|
|
278
297
|
type="button"
|
|
279
298
|
aria-label="icon-close"
|
|
299
|
+
disabled={disabled}
|
|
280
300
|
style={{
|
|
281
301
|
marginLeft: 8,
|
|
282
302
|
marginRight: 8,
|
|
283
303
|
flex: '0 0 auto',
|
|
284
|
-
cursor: 'pointer',
|
|
304
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
285
305
|
background: 'transparent',
|
|
286
306
|
border: 0,
|
|
287
307
|
padding: 0,
|
|
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
298
318
|
</div>
|
|
299
319
|
|
|
300
320
|
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
|
|
301
|
-
<Button
|
|
321
|
+
<Button
|
|
322
|
+
style={{ padding: 0 }}
|
|
323
|
+
type="link"
|
|
324
|
+
size="small"
|
|
325
|
+
icon={<PlusOutlined />}
|
|
326
|
+
onClick={handleAddCondition}
|
|
327
|
+
disabled={disabled}
|
|
328
|
+
>
|
|
302
329
|
{t('Add condition')}
|
|
303
330
|
</Button>
|
|
304
331
|
<Button
|
|
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
307
334
|
size="small"
|
|
308
335
|
icon={<PlusOutlined />}
|
|
309
336
|
onClick={handleAddConditionGroup}
|
|
337
|
+
disabled={disabled}
|
|
310
338
|
>
|
|
311
339
|
{t('Add condition group')}
|
|
312
340
|
</Button>
|