@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
|
@@ -68,6 +68,12 @@ interface LinkageRule {
|
|
|
68
68
|
}[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export function updateLinkageRules<T>(rules: T[], updater: (nextRules: T[]) => void): T[] {
|
|
72
|
+
const nextRules = _.cloneDeep(rules);
|
|
73
|
+
updater(nextRules);
|
|
74
|
+
return nextRules;
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
const previewValueForLog = (value: any) => {
|
|
72
78
|
if (value == null) return value;
|
|
73
79
|
const t = typeof value;
|
|
@@ -731,6 +737,41 @@ export const linkageSetMenuItemProps = defineAction({
|
|
|
731
737
|
},
|
|
732
738
|
});
|
|
733
739
|
|
|
740
|
+
export const linkageSetTabProps = defineAction({
|
|
741
|
+
name: 'linkageSetTabProps',
|
|
742
|
+
title: tExpr('Set tab state'),
|
|
743
|
+
scene: ActionScene.TAB_LINKAGE_RULES,
|
|
744
|
+
sort: 100,
|
|
745
|
+
uiSchema: {
|
|
746
|
+
value: {
|
|
747
|
+
type: 'string',
|
|
748
|
+
'x-component': (props) => {
|
|
749
|
+
const { value, onChange } = props;
|
|
750
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
751
|
+
const ctx = useFlowContext();
|
|
752
|
+
const t = ctx.model.translate.bind(ctx.model);
|
|
753
|
+
|
|
754
|
+
return (
|
|
755
|
+
<Select
|
|
756
|
+
value={value}
|
|
757
|
+
onChange={onChange}
|
|
758
|
+
placeholder={t('Please select state')}
|
|
759
|
+
style={{ width: '100%' }}
|
|
760
|
+
options={[
|
|
761
|
+
{ label: t('Visible'), value: 'visible' },
|
|
762
|
+
{ label: t('Hidden'), value: 'hidden' },
|
|
763
|
+
]}
|
|
764
|
+
allowClear
|
|
765
|
+
/>
|
|
766
|
+
);
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
handler(ctx, { value, setProps }) {
|
|
771
|
+
setProps(ctx.model, { hiddenModel: value === 'hidden' });
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
|
|
734
775
|
export const linkageSetFieldProps = defineAction({
|
|
735
776
|
name: 'linkageSetFieldProps',
|
|
736
777
|
title: tExpr('Set field state'),
|
|
@@ -1339,6 +1380,7 @@ export const linkageRunjs = defineAction({
|
|
|
1339
1380
|
ActionScene.FIELD_LINKAGE_RULES,
|
|
1340
1381
|
ActionScene.ACTION_LINKAGE_RULES,
|
|
1341
1382
|
ActionScene.MENU_LINKAGE_RULES,
|
|
1383
|
+
ActionScene.TAB_LINKAGE_RULES,
|
|
1342
1384
|
ActionScene.DETAILS_FIELD_LINKAGE_RULES,
|
|
1343
1385
|
ActionScene.SUB_FORM_FIELD_LINKAGE_RULES,
|
|
1344
1386
|
],
|
|
@@ -1464,13 +1506,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
|
|
|
1464
1506
|
}
|
|
1465
1507
|
|
|
1466
1508
|
const LinkageRulesUI = observer(
|
|
1467
|
-
(props: {
|
|
1468
|
-
|
|
1509
|
+
(props: {
|
|
1510
|
+
readonly value: LinkageRule[];
|
|
1511
|
+
onChange?: (value: LinkageRule[]) => void;
|
|
1512
|
+
supportedActions: string[];
|
|
1513
|
+
title?: string;
|
|
1514
|
+
}) => {
|
|
1515
|
+
const { value: rules = [], onChange, supportedActions } = props;
|
|
1469
1516
|
const ctx = useFlowContext();
|
|
1470
1517
|
const flowEngine = useFlowEngine();
|
|
1471
1518
|
const t = ctx.model.translate.bind(ctx.model);
|
|
1472
1519
|
const assignPriorityTip = t('Assignment takes precedence over form field assignment');
|
|
1473
1520
|
|
|
1521
|
+
const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
|
|
1522
|
+
if (onChange) {
|
|
1523
|
+
onChange(updateLinkageRules(rules, updater));
|
|
1524
|
+
} else {
|
|
1525
|
+
updater(rules);
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1474
1529
|
// 创建新规则的默认值
|
|
1475
1530
|
const createNewRule = (): LinkageRule => ({
|
|
1476
1531
|
key: uid(),
|
|
@@ -1487,7 +1542,7 @@ const LinkageRulesUI = observer(
|
|
|
1487
1542
|
|
|
1488
1543
|
// 删除规则
|
|
1489
1544
|
const handleDeleteRule = (index: number) => {
|
|
1490
|
-
|
|
1545
|
+
replaceRules((nextRules) => nextRules.splice(index, 1));
|
|
1491
1546
|
};
|
|
1492
1547
|
|
|
1493
1548
|
// 上移规则
|
|
@@ -1526,7 +1581,9 @@ const LinkageRulesUI = observer(
|
|
|
1526
1581
|
|
|
1527
1582
|
// 切换规则启用状态
|
|
1528
1583
|
const handleToggleEnable = (index: number, enable: boolean) => {
|
|
1529
|
-
|
|
1584
|
+
replaceRules((nextRules) => {
|
|
1585
|
+
nextRules[index].enable = enable;
|
|
1586
|
+
});
|
|
1530
1587
|
};
|
|
1531
1588
|
|
|
1532
1589
|
// 获取可用的动作类型
|
|
@@ -2169,17 +2226,30 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2169
2226
|
}
|
|
2170
2227
|
: props;
|
|
2171
2228
|
|
|
2172
|
-
//
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2229
|
+
// 只记录联动实际控制的属性,避免之后恢复状态时把标题、路由等无关的新配置回滚到旧快照。
|
|
2230
|
+
const originalProps = model.__originalProps || (model.__originalProps = {});
|
|
2231
|
+
const rememberOriginalProp = (key: string, value: unknown) => {
|
|
2232
|
+
if (!Object.prototype.hasOwnProperty.call(originalProps, key)) {
|
|
2233
|
+
originalProps[key] = value;
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
Object.keys(normalizedProps || {}).forEach((key) => {
|
|
2237
|
+
if (key === 'hiddenModel') {
|
|
2238
|
+
rememberOriginalProp(
|
|
2239
|
+
key,
|
|
2240
|
+
Object.prototype.hasOwnProperty.call(model.props || {}, key) ? model.props?.[key] : model.hidden,
|
|
2241
|
+
);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
rememberOriginalProp(key, model.props?.[key]);
|
|
2246
|
+
if (key === 'hiddenText' && normalizedProps[key]) {
|
|
2247
|
+
rememberOriginalProp('title', model.props?.title);
|
|
2248
|
+
}
|
|
2249
|
+
if (key === 'required') {
|
|
2250
|
+
rememberOriginalProp('rules', model.props?.rules);
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2183
2253
|
|
|
2184
2254
|
// 临时存起来,遍历完所有规则后,再统一处理
|
|
2185
2255
|
patchPropsByModel.set(model, {
|
|
@@ -2237,7 +2307,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2237
2307
|
const newProps = { ...model.__originalProps, ...patchProps };
|
|
2238
2308
|
const prevHidden = !!model.hidden;
|
|
2239
2309
|
const nextHidden = !!newProps.hiddenModel;
|
|
2240
|
-
|
|
2241
2310
|
model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
|
|
2242
2311
|
syncFieldOptionsToForks(model, patchProps);
|
|
2243
2312
|
if (typeof model.setHidden === 'function') {
|
|
@@ -2463,6 +2532,32 @@ export const menuLinkageRules = defineAction({
|
|
|
2463
2532
|
},
|
|
2464
2533
|
});
|
|
2465
2534
|
|
|
2535
|
+
export const tabLinkageRules = defineAction({
|
|
2536
|
+
name: 'tabLinkageRules',
|
|
2537
|
+
title: tExpr('Tab linkage rules'),
|
|
2538
|
+
uiMode: 'embed',
|
|
2539
|
+
uiSchema(ctx) {
|
|
2540
|
+
return {
|
|
2541
|
+
value: {
|
|
2542
|
+
type: 'array',
|
|
2543
|
+
'x-component': LinkageRulesUI,
|
|
2544
|
+
'x-component-props': {
|
|
2545
|
+
supportedActions: getSupportedActions(ctx, ActionScene.TAB_LINKAGE_RULES),
|
|
2546
|
+
title: tExpr('Tab linkage rules'),
|
|
2547
|
+
},
|
|
2548
|
+
},
|
|
2549
|
+
};
|
|
2550
|
+
},
|
|
2551
|
+
defaultParams: {
|
|
2552
|
+
value: [],
|
|
2553
|
+
},
|
|
2554
|
+
useRawParams: true,
|
|
2555
|
+
handler: async (ctx, params) => {
|
|
2556
|
+
const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
|
|
2557
|
+
return commonLinkageRulesHandler(ctx, resolved);
|
|
2558
|
+
},
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2466
2561
|
export const fieldLinkageRules = defineAction({
|
|
2467
2562
|
name: 'fieldLinkageRules',
|
|
2468
2563
|
title: tExpr('Field linkage rules'),
|
|
@@ -712,7 +712,6 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
712
712
|
<MobileMenuControlContext.Provider value={{ closeMobileMenu }}>
|
|
713
713
|
<DndProvider collisionDetection={menuCollisionDetection} onDragEnd={handleMenuDragEnd}>
|
|
714
714
|
<ProLayout
|
|
715
|
-
key={`admin-layout-menu-${menuRouteRefreshVersion}`}
|
|
716
715
|
{...props}
|
|
717
716
|
contentStyle={contentStyle}
|
|
718
717
|
siderWidth={customToken.siderWidth || 200}
|
|
@@ -11,23 +11,58 @@ import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
|
11
11
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
12
12
|
import React, { useEffect } from 'react';
|
|
13
13
|
import { createMemoryRouter, RouterProvider, useParams } from 'react-router-dom';
|
|
14
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
15
|
+
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../../flow-compat';
|
|
15
16
|
import type { LayoutDefinition } from '../../../../layout-manager/types';
|
|
16
17
|
import { AdminLayoutComponent } from '../AdminLayoutComponent';
|
|
17
18
|
import { AdminLayoutModel } from '../AdminLayoutModel';
|
|
19
|
+
import { AdminLayoutMenuItemModel } from '../AdminLayoutMenuModels';
|
|
18
20
|
import { AdminLayoutContent } from '../AdminLayoutSlotModels';
|
|
19
21
|
|
|
22
|
+
const proLayoutLifecycle = vi.hoisted(() => ({
|
|
23
|
+
events: [] as string[],
|
|
24
|
+
routes: [] as Array<{ children?: Array<{ name?: React.ReactNode }> }>,
|
|
25
|
+
}));
|
|
26
|
+
|
|
20
27
|
vi.mock('@ant-design/pro-layout', async () => {
|
|
21
28
|
const ReactModule = await import('react');
|
|
22
29
|
const RouteContext = ReactModule.createContext({ isMobile: false });
|
|
30
|
+
const ProLayoutMock = (props: {
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
route?: { children?: Array<{ name?: React.ReactNode }> };
|
|
33
|
+
}) => {
|
|
34
|
+
ReactModule.useEffect(() => {
|
|
35
|
+
proLayoutLifecycle.events.push('pro-layout:mount');
|
|
36
|
+
return () => {
|
|
37
|
+
proLayoutLifecycle.events.push('pro-layout:unmount');
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
proLayoutLifecycle.routes.push(props.route || {});
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
return ReactModule.createElement(
|
|
43
|
+
RouteContext.Provider,
|
|
44
|
+
{ value: { isMobile: false } },
|
|
26
45
|
ReactModule.createElement(
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
ReactModule.createElement(
|
|
46
|
+
'div',
|
|
47
|
+
{ 'data-testid': 'pro-layout' },
|
|
48
|
+
ReactModule.createElement(
|
|
49
|
+
'nav',
|
|
50
|
+
{ 'data-testid': 'pro-layout-menu' },
|
|
51
|
+
(props.route?.children || []).map((item) =>
|
|
52
|
+
ReactModule.createElement(
|
|
53
|
+
'span',
|
|
54
|
+
{ key: String(item.name), 'data-testid': 'pro-layout-menu-item' },
|
|
55
|
+
item.name,
|
|
56
|
+
),
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
props.children,
|
|
30
60
|
),
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
default: ProLayoutMock,
|
|
31
66
|
RouteContext,
|
|
32
67
|
};
|
|
33
68
|
});
|
|
@@ -53,6 +88,10 @@ vi.mock('../AppListRender', () => ({
|
|
|
53
88
|
useAppListRender: () => undefined,
|
|
54
89
|
}));
|
|
55
90
|
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
delete (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__;
|
|
93
|
+
});
|
|
94
|
+
|
|
56
95
|
describe('AdminLayoutComponent', () => {
|
|
57
96
|
it('keeps custom admin layout pages alive by layout route name', async () => {
|
|
58
97
|
const layout: LayoutDefinition = {
|
|
@@ -185,4 +224,212 @@ describe('AdminLayoutComponent', () => {
|
|
|
185
224
|
|
|
186
225
|
expect(deactivateLayout).toHaveBeenCalledTimes(1);
|
|
187
226
|
});
|
|
227
|
+
|
|
228
|
+
it('keeps layout content mounted when the menu route tree refreshes', async () => {
|
|
229
|
+
proLayoutLifecycle.events = [];
|
|
230
|
+
proLayoutLifecycle.routes = [];
|
|
231
|
+
const layout: LayoutDefinition = {
|
|
232
|
+
routeName: 'admin',
|
|
233
|
+
routePath: '/admin',
|
|
234
|
+
rootRouteName: 'admin',
|
|
235
|
+
uid: 'admin-layout-model',
|
|
236
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
237
|
+
rootPageModelClass: 'RootPageModel',
|
|
238
|
+
childPageModelClass: 'ChildPageModel',
|
|
239
|
+
authCheck: true,
|
|
240
|
+
};
|
|
241
|
+
const events: string[] = [];
|
|
242
|
+
const engine = new FlowEngine();
|
|
243
|
+
engine.context.defineProperty('routeRepository', {
|
|
244
|
+
value: {
|
|
245
|
+
activateLayout: vi.fn(() => vi.fn()),
|
|
246
|
+
ensureAccessibleLoaded: vi.fn(async () => []),
|
|
247
|
+
listAccessible: () => [],
|
|
248
|
+
subscribe: vi.fn(),
|
|
249
|
+
unsubscribe: vi.fn(),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
engine.context.defineProperty('t', {
|
|
253
|
+
value: (key: string) => key,
|
|
254
|
+
});
|
|
255
|
+
const model = engine.createModel<AdminLayoutModel>({
|
|
256
|
+
uid: layout.uid,
|
|
257
|
+
use: AdminLayoutModel,
|
|
258
|
+
props: {
|
|
259
|
+
layout,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const Page = () => {
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
events.push('page:mount');
|
|
266
|
+
return () => {
|
|
267
|
+
events.push('page:unmount');
|
|
268
|
+
};
|
|
269
|
+
}, []);
|
|
270
|
+
|
|
271
|
+
return <div data-testid="custom-admin-route">Workflow tasks</div>;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const router = createMemoryRouter(
|
|
275
|
+
[
|
|
276
|
+
{
|
|
277
|
+
id: layout.routeName,
|
|
278
|
+
path: layout.routePath,
|
|
279
|
+
element: <AdminLayoutComponent model={model} />,
|
|
280
|
+
children: [
|
|
281
|
+
{
|
|
282
|
+
id: 'admin.workflow.tasks',
|
|
283
|
+
path: 'workflow/tasks/:taskType?/:status?/:popupId?',
|
|
284
|
+
element: <Page />,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
{
|
|
290
|
+
initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
render(
|
|
295
|
+
<FlowEngineProvider engine={engine}>
|
|
296
|
+
<RouterProvider router={router} />
|
|
297
|
+
</FlowEngineProvider>,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(await screen.findByTestId('custom-admin-route')).toBeInTheDocument();
|
|
301
|
+
expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
|
|
302
|
+
|
|
303
|
+
act(() => {
|
|
304
|
+
model.refreshMenuRouteTree();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await act(async () => {
|
|
308
|
+
await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
|
|
312
|
+
expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
|
|
313
|
+
expect(events).toEqual(['page:mount']);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('updates ProLayout menu routes after accessible routes change without remounting the layout', async () => {
|
|
317
|
+
proLayoutLifecycle.events = [];
|
|
318
|
+
proLayoutLifecycle.routes = [];
|
|
319
|
+
(window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__ = 'v2';
|
|
320
|
+
const layout: LayoutDefinition = {
|
|
321
|
+
routeName: 'admin',
|
|
322
|
+
routePath: '/admin',
|
|
323
|
+
rootRouteName: 'admin',
|
|
324
|
+
uid: 'admin-layout-model',
|
|
325
|
+
layoutModelClass: 'AdminLayoutModel',
|
|
326
|
+
rootPageModelClass: 'RootPageModel',
|
|
327
|
+
childPageModelClass: 'ChildPageModel',
|
|
328
|
+
authCheck: true,
|
|
329
|
+
};
|
|
330
|
+
let accessibleRoutes: NocoBaseDesktopRoute[] = [
|
|
331
|
+
{
|
|
332
|
+
id: 1,
|
|
333
|
+
title: 'Visible page',
|
|
334
|
+
schemaUid: 'visible-page',
|
|
335
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
let routeRepositorySubscriber: (() => void) | undefined;
|
|
339
|
+
const events: string[] = [];
|
|
340
|
+
const engine = new FlowEngine();
|
|
341
|
+
engine.registerModels({
|
|
342
|
+
AdminLayoutMenuItemModel,
|
|
343
|
+
});
|
|
344
|
+
engine.context.defineProperty('routeRepository', {
|
|
345
|
+
value: {
|
|
346
|
+
activateLayout: vi.fn(() => vi.fn()),
|
|
347
|
+
ensureAccessibleLoaded: vi.fn(async () => accessibleRoutes),
|
|
348
|
+
listAccessible: () => accessibleRoutes,
|
|
349
|
+
subscribe: vi.fn((subscriber: () => void) => {
|
|
350
|
+
routeRepositorySubscriber = subscriber;
|
|
351
|
+
}),
|
|
352
|
+
unsubscribe: vi.fn(),
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
engine.context.defineProperty('app', {
|
|
356
|
+
value: {
|
|
357
|
+
getPublicPath: () => '/v/',
|
|
358
|
+
router: {
|
|
359
|
+
getBasename: () => '/v',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
engine.context.defineProperty('t', {
|
|
364
|
+
value: (key: string) => key,
|
|
365
|
+
});
|
|
366
|
+
const model = engine.createModel<AdminLayoutModel>({
|
|
367
|
+
uid: layout.uid,
|
|
368
|
+
use: AdminLayoutModel,
|
|
369
|
+
props: {
|
|
370
|
+
layout,
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const Page = () => {
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
events.push('page:mount');
|
|
377
|
+
return () => {
|
|
378
|
+
events.push('page:unmount');
|
|
379
|
+
};
|
|
380
|
+
}, []);
|
|
381
|
+
|
|
382
|
+
return <div data-testid="custom-admin-route">Workflow tasks</div>;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const router = createMemoryRouter(
|
|
386
|
+
[
|
|
387
|
+
{
|
|
388
|
+
id: layout.routeName,
|
|
389
|
+
path: layout.routePath,
|
|
390
|
+
element: <AdminLayoutComponent model={model} />,
|
|
391
|
+
children: [
|
|
392
|
+
{
|
|
393
|
+
id: 'admin.workflow.tasks',
|
|
394
|
+
path: 'workflow/tasks/:taskType?/:status?/:popupId?',
|
|
395
|
+
element: <Page />,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
{
|
|
401
|
+
initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
|
|
402
|
+
},
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
render(
|
|
406
|
+
<FlowEngineProvider engine={engine}>
|
|
407
|
+
<RouterProvider router={router} />
|
|
408
|
+
</FlowEngineProvider>,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
expect(await screen.findByText('Visible page')).toBeInTheDocument();
|
|
412
|
+
expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
|
|
413
|
+
|
|
414
|
+
act(() => {
|
|
415
|
+
accessibleRoutes = [];
|
|
416
|
+
routeRepositorySubscriber?.();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await waitFor(() => {
|
|
420
|
+
expect(screen.queryByText('Visible page')).not.toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await act(async () => {
|
|
424
|
+
await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
|
|
428
|
+
expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
|
|
429
|
+
expect(events).toEqual(['page:mount']);
|
|
430
|
+
expect(
|
|
431
|
+
proLayoutLifecycle.routes.some((route) => route.children?.some((item) => item.name === 'Visible page')),
|
|
432
|
+
).toBe(true);
|
|
433
|
+
expect(proLayoutLifecycle.routes.at(-1)?.children?.some((item) => item.name === 'Visible page')).toBe(false);
|
|
434
|
+
});
|
|
188
435
|
});
|
|
@@ -26,6 +26,10 @@ export default function useStyle() {
|
|
|
26
26
|
() => ({
|
|
27
27
|
[`.${COMPONENT_CLS}`]: {
|
|
28
28
|
'.vditor-reset': { fontSize: `${token.fontSize}px !important`, color: 'unset' },
|
|
29
|
+
'.vditor-reset h2': {
|
|
30
|
+
borderBottom: 'none !important',
|
|
31
|
+
boxShadow: 'none !important',
|
|
32
|
+
},
|
|
29
33
|
'.vditor': {
|
|
30
34
|
borderRadius: 8,
|
|
31
35
|
},
|
|
@@ -150,7 +150,7 @@ export const BlockItemCard = React.forwardRef(
|
|
|
150
150
|
) => {
|
|
151
151
|
const { t } = useTranslation();
|
|
152
152
|
const { token } = theme.useToken();
|
|
153
|
-
const { title: blockTitle, description, children, className, heightMode, ...rest } = props;
|
|
153
|
+
const { title: blockTitle, description, children, className, heightMode, style, ...rest } = props;
|
|
154
154
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
155
155
|
const setCardRef = useCallback(
|
|
156
156
|
(node: HTMLDivElement | null) => {
|
|
@@ -185,7 +185,7 @@ export const BlockItemCard = React.forwardRef(
|
|
|
185
185
|
<Card
|
|
186
186
|
ref={setCardRef as any}
|
|
187
187
|
title={title}
|
|
188
|
-
style={{ display: 'flex', flexDirection: 'column', height: height }}
|
|
188
|
+
style={{ display: 'flex', flexDirection: 'column', ...style, height: height ?? style?.height }}
|
|
189
189
|
styles={{
|
|
190
190
|
body: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'auto' },
|
|
191
191
|
header: {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import React, { useMemo, useState } from 'react';
|
|
10
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
11
11
|
import { DatePicker, Select, Space } from 'antd';
|
|
12
12
|
import { inferPickerType } from '../../flow-compat';
|
|
13
13
|
import { dayjs, getDateTimeFormat, getPickerFormat } from '@nocobase/utils/client';
|
|
@@ -29,6 +29,7 @@ export interface FieldAssignExactDatePickerProps {
|
|
|
29
29
|
isRange?: boolean;
|
|
30
30
|
onChange?: (value: ExactDatePickerValue) => void;
|
|
31
31
|
style?: React.CSSProperties;
|
|
32
|
+
disabled?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function getRawSingleValue(value: unknown, isRange: boolean): unknown {
|
|
@@ -114,7 +115,7 @@ function parseRangeValue(value: unknown, format: string): [dayjs.Dayjs, dayjs.Da
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProps> = (props) => {
|
|
117
|
-
const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style } = props;
|
|
118
|
+
const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style, disabled } = props;
|
|
118
119
|
const flowCtx = useFlowContext();
|
|
119
120
|
const t = flowCtx.model.translate.bind(flowCtx.model);
|
|
120
121
|
|
|
@@ -124,17 +125,17 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
|
|
|
124
125
|
return inferPickerFromRawValue(rawSingleValue, picker);
|
|
125
126
|
});
|
|
126
127
|
|
|
127
|
-
const getResolvedFormat = (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
() => getResolvedFormat(targetPicker),
|
|
135
|
-
[targetPicker, format, picker, showTime, timeFormat],
|
|
128
|
+
const getResolvedFormat = useCallback(
|
|
129
|
+
(nextPicker: ExactDatePickerMode) => {
|
|
130
|
+
const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
|
|
131
|
+
const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
|
|
132
|
+
return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
|
|
133
|
+
},
|
|
134
|
+
[format, picker, showTime, timeFormat],
|
|
136
135
|
);
|
|
137
136
|
|
|
137
|
+
const resolvedFormat = useMemo(() => getResolvedFormat(targetPicker), [getResolvedFormat, targetPicker]);
|
|
138
|
+
|
|
138
139
|
const singleValue = useMemo(() => {
|
|
139
140
|
if (isRange) return null;
|
|
140
141
|
return parseDateFromRawValue(value, resolvedFormat);
|
|
@@ -156,6 +157,10 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
|
|
|
156
157
|
);
|
|
157
158
|
|
|
158
159
|
const handlePickerTypeChange = (nextPicker: ExactDatePickerMode) => {
|
|
160
|
+
if (disabled) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
159
164
|
setTargetPicker(nextPicker);
|
|
160
165
|
|
|
161
166
|
if (!onChange) {
|
|
@@ -182,7 +187,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
|
|
|
182
187
|
inputReadOnly: flowCtx.isMobileLayout,
|
|
183
188
|
showTime: showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false,
|
|
184
189
|
value: singleValue,
|
|
190
|
+
disabled,
|
|
185
191
|
onChange: (nextDate: dayjs.Dayjs | null) => {
|
|
192
|
+
if (disabled) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
186
195
|
if (nextDate && dayjs.isDayjs(nextDate)) {
|
|
187
196
|
onChange?.(nextDate);
|
|
188
197
|
return;
|
|
@@ -200,7 +209,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
|
|
|
200
209
|
inputReadOnly: flowCtx.isMobileLayout,
|
|
201
210
|
showTime: showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')] } : false,
|
|
202
211
|
value: rangeValue,
|
|
212
|
+
disabled,
|
|
203
213
|
onChange: (nextDates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => {
|
|
214
|
+
if (disabled) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
204
217
|
if (Array.isArray(nextDates) && nextDates[0] && nextDates[1]) {
|
|
205
218
|
onChange?.([nextDates[0], nextDates[1]]);
|
|
206
219
|
return;
|
|
@@ -218,6 +231,7 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
|
|
|
218
231
|
value={targetPicker}
|
|
219
232
|
options={pickerOptions}
|
|
220
233
|
onChange={handlePickerTypeChange}
|
|
234
|
+
disabled={disabled}
|
|
221
235
|
/>
|
|
222
236
|
{isRange ? <DatePicker.RangePicker {...rangePickerProps} /> : <DatePicker {...singlePickerProps} />}
|
|
223
237
|
</Space.Compact>
|