@nocobase/client-v2 2.2.0-alpha.6 → 2.2.0-alpha.8
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/BaseApplication.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
- package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
- package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +7 -0
- 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 +1 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
- package/es/index.mjs +89 -89
- package/lib/index.js +97 -97
- package/package.json +8 -7
- package/src/BaseApplication.tsx +11 -6
- package/src/__tests__/app.test.tsx +55 -0
- package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
- package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
- package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
- package/src/components/form/TypedVariableInput.tsx +12 -8
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +44 -3
- package/src/flow/FlowPage.tsx +9 -1
- package/src/flow/__tests__/FlowPage.test.tsx +50 -3
- package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- 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/components/FieldAssignExactDatePicker.tsx +25 -11
- package/src/flow/components/FieldAssignValueInput.tsx +81 -15
- package/src/flow/components/FlowRoute.tsx +7 -3
- package/src/flow/components/RunJSValueEditor.tsx +9 -1
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +139 -6
- 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 +21 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -0
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
package/src/flow/FlowPage.tsx
CHANGED
|
@@ -60,7 +60,9 @@ type FlowPageProps = {
|
|
|
60
60
|
type FlowPageViewContext = FlowEngineContext & {
|
|
61
61
|
view?: {
|
|
62
62
|
inputArgs?: {
|
|
63
|
+
filterByTk?: unknown;
|
|
63
64
|
isMobileLayout?: unknown;
|
|
65
|
+
sourceId?: unknown;
|
|
64
66
|
};
|
|
65
67
|
};
|
|
66
68
|
};
|
|
@@ -102,6 +104,10 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
|
|
|
102
104
|
subType: 'object',
|
|
103
105
|
use: pageModelClass,
|
|
104
106
|
};
|
|
107
|
+
const isRuntimeRecordScopedPage =
|
|
108
|
+
!flowEngine.context.flowSettingsEnabled &&
|
|
109
|
+
(typeof ctx?.view?.inputArgs?.filterByTk !== 'undefined' ||
|
|
110
|
+
typeof ctx?.view?.inputArgs?.sourceId !== 'undefined');
|
|
105
111
|
if (shouldInjectDefaultChildTab) {
|
|
106
112
|
const tabTitle = defaultTabTitle || flowEngine.translate?.('Details');
|
|
107
113
|
options['subModels'] = {
|
|
@@ -128,7 +134,9 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
|
|
|
128
134
|
},
|
|
129
135
|
};
|
|
130
136
|
}
|
|
131
|
-
const data =
|
|
137
|
+
const data =
|
|
138
|
+
(isRuntimeRecordScopedPage && (await flowEngine.loadModel({ ...options, refresh: true }))) ||
|
|
139
|
+
(await flowEngine.loadOrCreateModel(options, { skipSave: !flowEngine.context.flowSettingsEnabled }));
|
|
132
140
|
if (data?.uid && onModelLoaded) {
|
|
133
141
|
data.context.addDelegate(ctx);
|
|
134
142
|
bindViewLayoutState(data, ctx);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { render, waitFor } from '@testing-library/react';
|
|
11
11
|
import React from 'react';
|
|
12
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { FlowPage } from '../FlowPage';
|
|
14
14
|
|
|
15
15
|
type TestContextProperty = {
|
|
@@ -71,10 +71,15 @@ const createPageModel = (): TestPageModel => {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
const mocks = vi.hoisted(() => ({
|
|
74
|
+
flowSettingsEnabled: false,
|
|
75
|
+
loadModel: vi.fn(),
|
|
76
|
+
loadOrCreateModel: vi.fn(),
|
|
74
77
|
rendererMobileStates: [] as boolean[],
|
|
75
78
|
rendererProps: undefined as RendererProps | undefined,
|
|
76
79
|
model: undefined as TestPageModel | undefined,
|
|
80
|
+
viewFilterByTk: undefined as unknown,
|
|
77
81
|
viewInputMobileLayout: true as boolean | undefined,
|
|
82
|
+
viewSourceId: undefined as unknown,
|
|
78
83
|
contextMobileLayout: undefined as boolean | undefined,
|
|
79
84
|
}));
|
|
80
85
|
|
|
@@ -94,15 +99,20 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
94
99
|
}),
|
|
95
100
|
useFlowEngine: vi.fn(() => ({
|
|
96
101
|
getModelClassAsync: vi.fn(async () => TestPageModelClass),
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
loadModel: mocks.loadModel,
|
|
103
|
+
loadOrCreateModel: mocks.loadOrCreateModel,
|
|
104
|
+
context: {
|
|
105
|
+
flowSettingsEnabled: mocks.flowSettingsEnabled,
|
|
106
|
+
},
|
|
99
107
|
translate: vi.fn((value: string) => value),
|
|
100
108
|
})),
|
|
101
109
|
useFlowModelById: vi.fn(() => mocks.model),
|
|
102
110
|
useFlowViewContext: vi.fn(() => ({
|
|
103
111
|
view: {
|
|
104
112
|
inputArgs: {
|
|
113
|
+
...(typeof mocks.viewFilterByTk !== 'undefined' ? { filterByTk: mocks.viewFilterByTk } : {}),
|
|
105
114
|
...(typeof mocks.viewInputMobileLayout === 'boolean' ? { isMobileLayout: mocks.viewInputMobileLayout } : {}),
|
|
115
|
+
...(typeof mocks.viewSourceId !== 'undefined' ? { sourceId: mocks.viewSourceId } : {}),
|
|
106
116
|
},
|
|
107
117
|
},
|
|
108
118
|
...(typeof mocks.contextMobileLayout === 'boolean' ? { isMobileLayout: mocks.contextMobileLayout } : {}),
|
|
@@ -154,11 +164,21 @@ vi.mock('ahooks', async () => {
|
|
|
154
164
|
});
|
|
155
165
|
|
|
156
166
|
describe('FlowPage', () => {
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
mocks.loadModel.mockImplementation(async () => null);
|
|
169
|
+
mocks.loadOrCreateModel.mockImplementation(async () => mocks.model);
|
|
170
|
+
});
|
|
171
|
+
|
|
157
172
|
afterEach(() => {
|
|
173
|
+
mocks.flowSettingsEnabled = false;
|
|
174
|
+
mocks.loadModel.mockReset();
|
|
175
|
+
mocks.loadOrCreateModel.mockReset();
|
|
158
176
|
mocks.rendererMobileStates = [];
|
|
159
177
|
mocks.rendererProps = undefined;
|
|
160
178
|
mocks.model = undefined;
|
|
179
|
+
mocks.viewFilterByTk = undefined;
|
|
161
180
|
mocks.viewInputMobileLayout = true;
|
|
181
|
+
mocks.viewSourceId = undefined;
|
|
162
182
|
mocks.contextMobileLayout = undefined;
|
|
163
183
|
});
|
|
164
184
|
|
|
@@ -203,4 +223,31 @@ describe('FlowPage', () => {
|
|
|
203
223
|
expect(mocks.rendererMobileStates[0]).toBe(expected);
|
|
204
224
|
},
|
|
205
225
|
);
|
|
226
|
+
|
|
227
|
+
it('reloads runtime page models for record scoped views', async () => {
|
|
228
|
+
mocks.model = createPageModel();
|
|
229
|
+
mocks.viewFilterByTk = 1;
|
|
230
|
+
mocks.loadModel.mockImplementation(async () => mocks.model);
|
|
231
|
+
|
|
232
|
+
render(<FlowPage onModelLoaded={vi.fn()} />);
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(mocks.loadModel).toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
expect(mocks.loadModel.mock.calls[0]?.[0]).toMatchObject({ refresh: true });
|
|
238
|
+
expect(mocks.loadOrCreateModel).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('keeps configured page model cache behavior in flow settings mode', async () => {
|
|
242
|
+
mocks.flowSettingsEnabled = true;
|
|
243
|
+
mocks.model = createPageModel();
|
|
244
|
+
mocks.viewFilterByTk = 1;
|
|
245
|
+
|
|
246
|
+
render(<FlowPage onModelLoaded={vi.fn()} />);
|
|
247
|
+
|
|
248
|
+
await waitFor(() => {
|
|
249
|
+
expect(mocks.loadOrCreateModel).toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
expect(mocks.loadModel).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
206
253
|
});
|
|
@@ -1666,7 +1666,7 @@ describe('FlowRoute', () => {
|
|
|
1666
1666
|
expect(engine.getModel('public-form-1')).toBeUndefined();
|
|
1667
1667
|
});
|
|
1668
1668
|
|
|
1669
|
-
it('should
|
|
1669
|
+
it('should skip accessible route loading when layout authCheck is false', async () => {
|
|
1670
1670
|
const engine = new FlowEngine();
|
|
1671
1671
|
const ensureAccessibleLoaded = vi.fn().mockRejectedValue(new Error('cannot load accessible routes'));
|
|
1672
1672
|
const getRouteBySchemaUid = vi.fn();
|
|
@@ -1735,7 +1735,7 @@ describe('FlowRoute', () => {
|
|
|
1735
1735
|
await waitFor(() => {
|
|
1736
1736
|
expect(layoutModel.registerRoutePage).toHaveBeenCalledWith('public-form-1', expect.any(Object));
|
|
1737
1737
|
});
|
|
1738
|
-
expect(ensureAccessibleLoaded).
|
|
1738
|
+
expect(ensureAccessibleLoaded).not.toHaveBeenCalled();
|
|
1739
1739
|
expect(getRouteBySchemaUid).not.toHaveBeenCalled();
|
|
1740
1740
|
});
|
|
1741
1741
|
|
|
@@ -135,11 +135,15 @@ describe('linkageRulesRefresh action', () => {
|
|
|
135
135
|
expect(handler).toHaveBeenCalledWith(ctx, { value: ['master-mounted'] });
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
it('
|
|
138
|
+
it('skips master model when forks can handle flow in design mode', async () => {
|
|
139
139
|
const handler = vi.fn(async () => {});
|
|
140
140
|
const model: any = {
|
|
141
141
|
isFork: false,
|
|
142
|
-
forks: new Set([
|
|
142
|
+
forks: new Set([
|
|
143
|
+
{
|
|
144
|
+
getFlow: vi.fn(() => ({})),
|
|
145
|
+
},
|
|
146
|
+
]),
|
|
143
147
|
getFlow: vi.fn(() => ({})),
|
|
144
148
|
getStepParams: vi.fn(() => ({ value: ['master'] })),
|
|
145
149
|
context: {
|
|
@@ -157,7 +161,7 @@ describe('linkageRulesRefresh action', () => {
|
|
|
157
161
|
flowKey: 'buttonSettings',
|
|
158
162
|
});
|
|
159
163
|
|
|
160
|
-
expect(handler).
|
|
164
|
+
expect(handler).not.toHaveBeenCalled();
|
|
161
165
|
});
|
|
162
166
|
|
|
163
167
|
it('runs linkage action on fork model and resolves params', async () => {
|
|
@@ -35,12 +35,8 @@ export const linkageRulesRefresh = defineAction({
|
|
|
35
35
|
// Prefer running on the current model; fallback to blockModel when the current model doesn't own the flow.
|
|
36
36
|
if (!hasFlow) return;
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// Skip master when unmounted forks can handle the same flow.
|
|
39
39
|
// Otherwise master is likely the rendered model and still needs refresh.
|
|
40
|
-
// In design mode, always refresh master so the currently edited model state stays in sync.
|
|
41
|
-
const flowSettingsEnabled = Boolean(
|
|
42
|
-
(ctx as any)?.flowSettingsEnabled || (model as any)?.context?.flowSettingsEnabled,
|
|
43
|
-
);
|
|
44
40
|
const hasForkWithFlow =
|
|
45
41
|
!model?.isFork &&
|
|
46
42
|
!!model?.forks?.size &&
|
|
@@ -49,7 +45,7 @@ export const linkageRulesRefresh = defineAction({
|
|
|
49
45
|
return !!fork?.getFlow?.(flowKey);
|
|
50
46
|
});
|
|
51
47
|
const isMasterMounted = Boolean((model as any)?.context?.ref?.current);
|
|
52
|
-
if (hasForkWithFlow && !isMasterMounted
|
|
48
|
+
if (hasForkWithFlow && !isMasterMounted) {
|
|
53
49
|
return;
|
|
54
50
|
}
|
|
55
51
|
|
|
@@ -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
|
});
|
|
@@ -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>
|