@nocobase/client-v2 2.1.0-alpha.45 → 2.1.0-alpha.46
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/authRedirect.d.ts +12 -0
- package/es/components/form/TypedVariableInput.d.ts +75 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/ConditionBuilder.d.ts +1 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
- package/es/flow/components/fieldAssignOptions.d.ts +10 -0
- package/es/flow/components/filter/LinkageFilterItem.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +1 -0
- package/es/flow/components/filter/metaTreeAssociationDepth.d.ts +13 -0
- package/es/flow/models/base/AssociationFieldGroupModel.d.ts +2 -33
- package/es/flow/models/blocks/assign-form/assignFieldValuesFlow.d.ts +8 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +2 -0
- package/es/index.mjs +103 -103
- package/lib/index.js +105 -105
- package/package.json +7 -7
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +5 -0
- package/src/authRedirect.ts +43 -0
- package/src/components/README.md +48 -0
- package/src/components/README.zh-CN.md +48 -0
- package/src/components/form/TypedVariableInput.tsx +441 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +152 -0
- package/src/components/form/index.tsx +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +8 -1
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +100 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +23 -1
- package/src/flow/actions/dataScope.tsx +8 -1
- package/src/flow/actions/linkageRules.tsx +8 -5
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -3
- package/src/flow/actions/setTargetDataScope.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +15 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +75 -1
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +11 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -4
- package/src/flow/common/Markdown/Edit.tsx +32 -4
- package/src/flow/components/ConditionBuilder.tsx +12 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +37 -46
- package/src/flow/components/FieldAssignValueInput.tsx +8 -4
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +112 -1
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +71 -1
- package/src/flow/components/code-editor/__tests__/runjsCompletions.test.ts +272 -27
- package/src/flow/components/code-editor/runjsCompletions.ts +490 -9
- package/src/flow/components/fieldAssignOptions.ts +23 -6
- package/src/flow/components/filter/LinkageFilterItem.tsx +22 -9
- package/src/flow/components/filter/VariableFilterItem.tsx +27 -9
- package/src/flow/components/filter/__tests__/fieldsToOptions.test.ts +48 -0
- package/src/flow/components/filter/__tests__/metaTreeAssociationDepth.test.ts +60 -0
- package/src/flow/components/filter/fieldsToOptions.ts +9 -4
- package/src/flow/components/filter/metaTreeAssociationDepth.ts +69 -0
- package/src/flow/models/actions/FilterActionModel.tsx +15 -3
- package/src/flow/models/actions/__tests__/AssignFormRefill.test.ts +90 -0
- package/src/flow/models/base/AssociationFieldGroupModel.tsx +20 -3
- package/src/flow/models/base/GridModel.tsx +1 -1
- package/src/flow/models/base/PageModel/ChildPageModel.tsx +5 -1
- package/src/flow/models/base/PageModel/__tests__/ChildPageModel.test.ts +12 -0
- package/src/flow/models/base/__tests__/AssociationFieldGroupModel.test.ts +86 -0
- package/src/flow/models/base/__tests__/GridModel.render.test.tsx +53 -0
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +12 -3
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -4
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/assignFieldValuesFlow.tsx +142 -40
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +1 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +6 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/connectFields.tsx +48 -6
- package/src/flow/models/blocks/form/FormAssociationItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +9 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +27 -2
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +29 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +5 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +15 -5
- package/src/flow/models/blocks/table/TableCustomColumnModel.tsx +2 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +18 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +6 -7
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +5 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +70 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FlowContext, FlowContextProvider } from '@nocobase/flow-engine';
|
|
11
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { TypedVariableInput } from '../TypedVariableInput';
|
|
15
|
+
|
|
16
|
+
function createContextWithEnv() {
|
|
17
|
+
const ctx = new FlowContext();
|
|
18
|
+
(ctx as any).t = (key: string) => key;
|
|
19
|
+
|
|
20
|
+
ctx.defineProperty('$env', {
|
|
21
|
+
value: { SMTP_PORT: 465, SECURE_FLAG: true },
|
|
22
|
+
meta: {
|
|
23
|
+
title: 'Env',
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
SMTP_PORT: { title: 'SMTP Port', type: 'number' },
|
|
27
|
+
SECURE_FLAG: { title: 'Secure flag', type: 'boolean' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return ctx;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderWithCtx(ctx: FlowContext, node: React.ReactNode) {
|
|
36
|
+
return render(<FlowContextProvider context={ctx}>{node}</FlowContextProvider>);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('TypedVariableInput - constant rendering', () => {
|
|
40
|
+
it('renders an InputNumber for a numeric value when types=[number]', async () => {
|
|
41
|
+
const ctx = createContextWithEnv();
|
|
42
|
+
renderWithCtx(
|
|
43
|
+
ctx,
|
|
44
|
+
<TypedVariableInput
|
|
45
|
+
value={465}
|
|
46
|
+
types={[['number', { min: 1, max: 65535 }]]}
|
|
47
|
+
namespaces={['$env']}
|
|
48
|
+
onChange={() => undefined}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
const numberInput = await screen.findByDisplayValue('465');
|
|
52
|
+
expect(numberInput).toBeInTheDocument();
|
|
53
|
+
expect(numberInput.getAttribute('role')).toBe('spinbutton');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders a boolean Select when value is true and types=[boolean]', async () => {
|
|
57
|
+
const ctx = createContextWithEnv();
|
|
58
|
+
renderWithCtx(
|
|
59
|
+
ctx,
|
|
60
|
+
<TypedVariableInput value={true} types={['boolean']} namespaces={['$env']} onChange={() => undefined} />,
|
|
61
|
+
);
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
// antd Select renders the chosen item label via a selection-item span
|
|
64
|
+
const item = screen.getByText('True');
|
|
65
|
+
expect(item).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders the Null placeholder when value=null and nullable=true', async () => {
|
|
70
|
+
const ctx = createContextWithEnv();
|
|
71
|
+
renderWithCtx(
|
|
72
|
+
ctx,
|
|
73
|
+
<TypedVariableInput value={null} types={['number']} namespaces={['$env']} nullable onChange={() => undefined} />,
|
|
74
|
+
);
|
|
75
|
+
// Uses placeholder slot (not value) so antd's grey placeholder colour
|
|
76
|
+
// applies — see comment in TypedVariableInput.tsx.
|
|
77
|
+
const nullInput = await screen.findByPlaceholderText('<Null>');
|
|
78
|
+
expect(nullInput).toBeInTheDocument();
|
|
79
|
+
expect(nullInput.getAttribute('readonly')).not.toBeNull();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('TypedVariableInput - variable rendering', () => {
|
|
84
|
+
it('renders a labelled pill when value is a {{ $env.X }} expression', async () => {
|
|
85
|
+
const ctx = createContextWithEnv();
|
|
86
|
+
renderWithCtx(
|
|
87
|
+
ctx,
|
|
88
|
+
<TypedVariableInput
|
|
89
|
+
value="{{$env.SMTP_PORT}}"
|
|
90
|
+
types={['number']}
|
|
91
|
+
namespaces={['$env']}
|
|
92
|
+
onChange={() => undefined}
|
|
93
|
+
/>,
|
|
94
|
+
);
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
const tag = screen.getByRole('button', { name: 'variable-tag' });
|
|
97
|
+
expect(tag.textContent).toContain('Env');
|
|
98
|
+
expect(tag.textContent).toContain('SMTP Port');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('clears back to null when the close button is clicked (nullable=true)', async () => {
|
|
103
|
+
const ctx = createContextWithEnv();
|
|
104
|
+
const handleChange = vi.fn();
|
|
105
|
+
renderWithCtx(
|
|
106
|
+
ctx,
|
|
107
|
+
<TypedVariableInput
|
|
108
|
+
value="{{$env.SMTP_PORT}}"
|
|
109
|
+
types={['number']}
|
|
110
|
+
namespaces={['$env']}
|
|
111
|
+
nullable
|
|
112
|
+
onChange={handleChange}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
const clear = await screen.findByRole('button', { name: 'icon-close' });
|
|
116
|
+
fireEvent.click(clear);
|
|
117
|
+
expect(handleChange).toHaveBeenCalledWith(null);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('clears back to default-of-first-type when nullable=false', async () => {
|
|
121
|
+
const ctx = createContextWithEnv();
|
|
122
|
+
const handleChange = vi.fn();
|
|
123
|
+
renderWithCtx(
|
|
124
|
+
ctx,
|
|
125
|
+
<TypedVariableInput
|
|
126
|
+
value="{{$env.SMTP_PORT}}"
|
|
127
|
+
types={['number']}
|
|
128
|
+
namespaces={['$env']}
|
|
129
|
+
nullable={false}
|
|
130
|
+
onChange={handleChange}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
const clear = await screen.findByRole('button', { name: 'icon-close' });
|
|
134
|
+
fireEvent.click(clear);
|
|
135
|
+
expect(handleChange).toHaveBeenCalledWith(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('TypedVariableInput - editor onChange propagation', () => {
|
|
140
|
+
it('forwards numeric edits via onChange', async () => {
|
|
141
|
+
const ctx = createContextWithEnv();
|
|
142
|
+
const handleChange = vi.fn();
|
|
143
|
+
renderWithCtx(ctx, <TypedVariableInput value={465} types={['number']} onChange={handleChange} />);
|
|
144
|
+
const numberInput = await screen.findByDisplayValue('465');
|
|
145
|
+
fireEvent.change(numberInput, { target: { value: '587' } });
|
|
146
|
+
// antd InputNumber may emit on blur; force blur to flush
|
|
147
|
+
fireEvent.blur(numberInput);
|
|
148
|
+
expect(handleChange).toHaveBeenCalled();
|
|
149
|
+
const lastCall = handleChange.mock.calls[handleChange.mock.calls.length - 1][0];
|
|
150
|
+
expect(lastCall).toBe(587);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
12
12
|
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
13
13
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
14
14
|
import { FlowContextProvider, FlowEngine, FlowEngineProvider, type FlowModel } from '@nocobase/flow-engine';
|
|
@@ -32,6 +32,13 @@ describe('FlowRoute', () => {
|
|
|
32
32
|
beforeEach(() => {
|
|
33
33
|
vi.clearAllMocks();
|
|
34
34
|
hookState.refresh = vi.fn();
|
|
35
|
+
// Fixtures mount the modern client under the `v2` segment; tell the
|
|
36
|
+
// runtime-prefix helper so v2-runtime detection matches.
|
|
37
|
+
(globalThis.window as any).__nocobase_modern_client_prefix__ = 'v2';
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
delete (globalThis.window as any).__nocobase_modern_client_prefix__;
|
|
35
42
|
});
|
|
36
43
|
|
|
37
44
|
it('should bridge page lifecycle to admin-layout-model', async () => {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { actionLinkageRules } from '../linkageRules';
|
|
12
|
+
|
|
13
|
+
function createRule(actions: any[]) {
|
|
14
|
+
return {
|
|
15
|
+
key: 'r1',
|
|
16
|
+
title: 'r1',
|
|
17
|
+
enable: true,
|
|
18
|
+
condition: { logic: '$and', items: [] },
|
|
19
|
+
actions,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('linkageRules hidden state propagation', () => {
|
|
24
|
+
it('restores hidden state to same-target models when reverting to original props', async () => {
|
|
25
|
+
const blockModel = { uid: 'block-1' };
|
|
26
|
+
|
|
27
|
+
const hostModel: any = {
|
|
28
|
+
uid: 'host',
|
|
29
|
+
hidden: false,
|
|
30
|
+
context: { blockModel },
|
|
31
|
+
__allModels: [],
|
|
32
|
+
setProps: vi.fn(),
|
|
33
|
+
getFlow: vi.fn(() => ({})),
|
|
34
|
+
getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
|
|
35
|
+
if (stepKey === 'init') return { fieldPath: 'org_m2o' };
|
|
36
|
+
return undefined;
|
|
37
|
+
}),
|
|
38
|
+
translate: (s: string) => s,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const sameTargetModel: any = {
|
|
42
|
+
uid: 'same-target',
|
|
43
|
+
hidden: false,
|
|
44
|
+
context: { blockModel },
|
|
45
|
+
setProps: vi.fn(),
|
|
46
|
+
getStepParams: vi.fn((_flowKey: string, stepKey: string) => {
|
|
47
|
+
if (stepKey === 'init') return { fieldPath: 'org_m2o' };
|
|
48
|
+
return undefined;
|
|
49
|
+
}),
|
|
50
|
+
translate: (s: string) => s,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const engine = {
|
|
54
|
+
forEachModel: (visitor: (m: any) => void) => {
|
|
55
|
+
[hostModel, sameTargetModel].forEach(visitor);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ctx: any = {
|
|
60
|
+
flowKey: 'buttonSettings',
|
|
61
|
+
model: hostModel,
|
|
62
|
+
engine,
|
|
63
|
+
app: {
|
|
64
|
+
jsonLogic: {
|
|
65
|
+
apply: vi.fn(() => true),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
resolveJsonTemplate: vi.fn(async (value: any) => value),
|
|
69
|
+
getAction: (name: string) => {
|
|
70
|
+
if (name !== 'linkageSetActionProps') return undefined;
|
|
71
|
+
return {
|
|
72
|
+
handler: async (_ctx: any, params: any) => {
|
|
73
|
+
params.setProps(hostModel, {
|
|
74
|
+
hiddenModel: params.value === 'hidden',
|
|
75
|
+
disabled: false,
|
|
76
|
+
hiddenText: false,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await actionLinkageRules.handler(ctx, {
|
|
84
|
+
value: [
|
|
85
|
+
createRule([
|
|
86
|
+
{
|
|
87
|
+
name: 'linkageSetActionProps',
|
|
88
|
+
params: { value: 'hidden' },
|
|
89
|
+
},
|
|
90
|
+
]),
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
expect(hostModel.hidden).toBe(true);
|
|
94
|
+
expect(sameTargetModel.hidden).toBe(true);
|
|
95
|
+
|
|
96
|
+
await actionLinkageRules.handler(ctx, { value: [] });
|
|
97
|
+
expect(hostModel.hidden).toBe(false);
|
|
98
|
+
expect(sameTargetModel.hidden).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -46,7 +46,7 @@ describe('linkageRulesRefresh action', () => {
|
|
|
46
46
|
expect(handler).not.toHaveBeenCalled();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
it('
|
|
49
|
+
it('skips master model when forks can handle flow even if regular action buttons do not attach refs', async () => {
|
|
50
50
|
const handler = vi.fn(async () => {});
|
|
51
51
|
const model: any = {
|
|
52
52
|
isFork: false,
|
|
@@ -74,6 +74,28 @@ describe('linkageRulesRefresh action', () => {
|
|
|
74
74
|
flowKey: 'buttonSettings',
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
expect(handler).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('runs linkage action on master model when no forks exist in runtime mode', async () => {
|
|
81
|
+
const handler = vi.fn(async () => {});
|
|
82
|
+
const model: any = {
|
|
83
|
+
isFork: false,
|
|
84
|
+
forks: new Set(),
|
|
85
|
+
getFlow: vi.fn(() => ({})),
|
|
86
|
+
getStepParams: vi.fn(() => ({ value: ['master-runtime'] })),
|
|
87
|
+
};
|
|
88
|
+
const ctx: any = {
|
|
89
|
+
model,
|
|
90
|
+
resolveJsonTemplate: vi.fn(async (p: any) => p),
|
|
91
|
+
getAction: vi.fn(() => ({ handler })),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
await linkageRulesRefresh.handler(ctx, {
|
|
95
|
+
actionName: 'actionLinkageRules',
|
|
96
|
+
flowKey: 'buttonSettings',
|
|
97
|
+
});
|
|
98
|
+
|
|
77
99
|
expect(handler).toHaveBeenCalledWith(ctx, { value: ['master-runtime'] });
|
|
78
100
|
});
|
|
79
101
|
|
|
@@ -33,7 +33,14 @@ export const dataScope = defineAction({
|
|
|
33
33
|
return (
|
|
34
34
|
<FilterGroup
|
|
35
35
|
value={props.value}
|
|
36
|
-
FilterItem={(p) =>
|
|
36
|
+
FilterItem={(p) => (
|
|
37
|
+
<VariableFilterItem
|
|
38
|
+
{...p}
|
|
39
|
+
model={flowContext.model}
|
|
40
|
+
rightAsVariable
|
|
41
|
+
maxAssociationFieldDepth={flowContext.model instanceof FieldModel ? undefined : 2}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
37
44
|
/>
|
|
38
45
|
);
|
|
39
46
|
},
|
|
@@ -1606,7 +1606,9 @@ const LinkageRulesUI = observer(
|
|
|
1606
1606
|
<div style={{ paddingLeft: 12 }}>
|
|
1607
1607
|
<FilterGroup
|
|
1608
1608
|
value={rule.condition}
|
|
1609
|
-
FilterItem={(props) =>
|
|
1609
|
+
FilterItem={(props) => (
|
|
1610
|
+
<LinkageFilterItem model={ctx.model} value={props.value} maxAssociationFieldDepth={2} />
|
|
1611
|
+
)}
|
|
1610
1612
|
/>
|
|
1611
1613
|
</div>
|
|
1612
1614
|
</div>
|
|
@@ -2099,16 +2101,17 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2099
2101
|
mergedByUid.forEach((model: any, uid) => {
|
|
2100
2102
|
const patchProps = mergedPropsByUid.get(uid) || {};
|
|
2101
2103
|
const newProps = { ...model.__originalProps, ...patchProps };
|
|
2102
|
-
const
|
|
2104
|
+
const prevHidden = !!model.hidden;
|
|
2105
|
+
const nextHidden = !!newProps.hiddenModel;
|
|
2103
2106
|
|
|
2104
2107
|
model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText']));
|
|
2105
2108
|
syncFieldOptionsToForks(model, patchProps);
|
|
2106
2109
|
if (typeof model.setHidden === 'function') {
|
|
2107
2110
|
model.setHidden(!!newProps.hiddenModel);
|
|
2108
2111
|
} else {
|
|
2109
|
-
model.hidden =
|
|
2110
|
-
if (
|
|
2111
|
-
hiddenStatePatches.push({ model, hidden:
|
|
2112
|
+
model.hidden = nextHidden;
|
|
2113
|
+
if (prevHidden !== nextHidden) {
|
|
2114
|
+
hiddenStatePatches.push({ model, hidden: nextHidden });
|
|
2112
2115
|
}
|
|
2113
2116
|
}
|
|
2114
2117
|
|
|
@@ -41,16 +41,15 @@ export const linkageRulesRefresh = defineAction({
|
|
|
41
41
|
const flowSettingsEnabled = Boolean(
|
|
42
42
|
(ctx as any)?.flowSettingsEnabled || (model as any)?.context?.flowSettingsEnabled,
|
|
43
43
|
);
|
|
44
|
-
const
|
|
44
|
+
const hasForkWithFlow =
|
|
45
45
|
!model?.isFork &&
|
|
46
46
|
!!model?.forks?.size &&
|
|
47
47
|
Array.from(model?.forks || []).some((fork: any) => {
|
|
48
48
|
if (!fork || fork.disposed) return false;
|
|
49
|
-
if (!fork?.context?.ref?.current) return false;
|
|
50
49
|
return !!fork?.getFlow?.(flowKey);
|
|
51
50
|
});
|
|
52
51
|
const isMasterMounted = Boolean((model as any)?.context?.ref?.current);
|
|
53
|
-
if (
|
|
52
|
+
if (hasForkWithFlow && !isMasterMounted && !flowSettingsEnabled) {
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
56
55
|
|
|
@@ -135,7 +135,7 @@ function SetTargetDataScope(props) {
|
|
|
135
135
|
return (
|
|
136
136
|
<FilterGroup
|
|
137
137
|
value={props.value}
|
|
138
|
-
FilterItem={(p) => <VariableFilterItem {...p} model={targetModel} rightAsVariable />}
|
|
138
|
+
FilterItem={(p) => <VariableFilterItem {...p} model={targetModel} rightAsVariable maxAssociationFieldDepth={2} />}
|
|
139
139
|
/>
|
|
140
140
|
);
|
|
141
141
|
}
|
|
@@ -55,6 +55,15 @@ const insertPositionToMethod = {
|
|
|
55
55
|
afterEnd: 'insertAfter',
|
|
56
56
|
} as const;
|
|
57
57
|
|
|
58
|
+
const omitMenuRuntimeProps = (props: Record<string, unknown> = {}) => {
|
|
59
|
+
const persistableProps = { ...props };
|
|
60
|
+
delete persistableProps.item;
|
|
61
|
+
delete persistableProps.dom;
|
|
62
|
+
delete persistableProps.options;
|
|
63
|
+
delete persistableProps.renderType;
|
|
64
|
+
return persistableProps;
|
|
65
|
+
};
|
|
66
|
+
|
|
58
67
|
export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStructure> {
|
|
59
68
|
private creationPersisted = false;
|
|
60
69
|
private persistedStateHydrated = false;
|
|
@@ -261,6 +270,12 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
261
270
|
return currentFlowCount > 0 || initialFlowCount > 0;
|
|
262
271
|
}
|
|
263
272
|
|
|
273
|
+
serialize(): ReturnType<FlowModel['serialize']> {
|
|
274
|
+
const data = super.serialize();
|
|
275
|
+
data.props = omitMenuRuntimeProps(data.props);
|
|
276
|
+
return data;
|
|
277
|
+
}
|
|
278
|
+
|
|
264
279
|
setHidden(value: boolean) {
|
|
265
280
|
const previous = this.hidden;
|
|
266
281
|
super.setHidden(value);
|
|
@@ -698,7 +713,6 @@ AdminLayoutMenuItemModel.registerFlow({
|
|
|
698
713
|
AdminLayoutMenuItemModel.registerFlow({
|
|
699
714
|
key: 'menuSettings',
|
|
700
715
|
title: 'Menu settings',
|
|
701
|
-
manual: true,
|
|
702
716
|
steps: {
|
|
703
717
|
edit: {
|
|
704
718
|
title: 'Edit',
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
14
14
|
import { observer } from '@nocobase/flow-engine';
|
|
15
15
|
import { MemoryRouter } from 'react-router-dom';
|
|
@@ -45,6 +45,9 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
45
45
|
let modalConfirmMock: ReturnType<typeof vi.fn>;
|
|
46
46
|
|
|
47
47
|
beforeEach(() => {
|
|
48
|
+
// Fixtures mount the modern client under the `v2` segment; tell the
|
|
49
|
+
// runtime-prefix helper so v2-runtime detection matches.
|
|
50
|
+
(globalThis.window as any).__nocobase_modern_client_prefix__ = 'v2';
|
|
48
51
|
engine = new FlowEngine();
|
|
49
52
|
modalConfirmMock = vi.fn().mockResolvedValue(true);
|
|
50
53
|
engine.registerModels({
|
|
@@ -104,6 +107,10 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
104
107
|
vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
105
108
|
});
|
|
106
109
|
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
delete (globalThis.window as any).__nocobase_modern_client_prefix__;
|
|
112
|
+
});
|
|
113
|
+
|
|
107
114
|
const createRoute = (options?: Partial<import('../../../../flow-compat').NocoBaseDesktopRoute>) => ({
|
|
108
115
|
id: 1,
|
|
109
116
|
title: 'Page 1',
|
|
@@ -950,6 +957,73 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
950
957
|
});
|
|
951
958
|
});
|
|
952
959
|
|
|
960
|
+
it('should save menu linkage rules without serializing runtime render props', async () => {
|
|
961
|
+
type SerializedFlowModel = Record<string, unknown> & {
|
|
962
|
+
props?: Record<string, unknown>;
|
|
963
|
+
subModels?: unknown;
|
|
964
|
+
};
|
|
965
|
+
const save = vi.fn(
|
|
966
|
+
async (targetModel: { serialize: () => SerializedFlowModel }, options?: { onlyStepParams?: boolean }) => {
|
|
967
|
+
const data = targetModel.serialize();
|
|
968
|
+
if (options?.onlyStepParams) {
|
|
969
|
+
delete data.subModels;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
expect(data.props?.item).toBeUndefined();
|
|
973
|
+
expect(data.props?.dom).toBeUndefined();
|
|
974
|
+
expect(data.props?.options).toBeUndefined();
|
|
975
|
+
expect(data.props?.renderType).toBeUndefined();
|
|
976
|
+
expect(() => JSON.stringify(data)).not.toThrow();
|
|
977
|
+
return data;
|
|
978
|
+
},
|
|
979
|
+
);
|
|
980
|
+
const updateRoute = vi.fn().mockResolvedValue(undefined);
|
|
981
|
+
engine.setModelRepository({ save } as Parameters<FlowEngine['setModelRepository']>[0]);
|
|
982
|
+
engine.context.routeRepository.updateRoute = updateRoute;
|
|
983
|
+
|
|
984
|
+
const route = createRoute();
|
|
985
|
+
const model = engine.createModel<AdminLayoutMenuItemModel>({
|
|
986
|
+
uid: 'menu-item-linkage-runtime-props',
|
|
987
|
+
use: AdminLayoutMenuItemModel,
|
|
988
|
+
props: {
|
|
989
|
+
route,
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
const item = {
|
|
993
|
+
name: 'Page 1',
|
|
994
|
+
path: '/admin/page-1',
|
|
995
|
+
_route: route,
|
|
996
|
+
_model: model,
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
model.setProps({
|
|
1000
|
+
item,
|
|
1001
|
+
dom: React.createElement('span', null, 'Page 1'),
|
|
1002
|
+
renderType: 'item',
|
|
1003
|
+
options: { collapsed: false },
|
|
1004
|
+
});
|
|
1005
|
+
model.setStepParams('menuSettings', 'linkageRules', {
|
|
1006
|
+
value: [
|
|
1007
|
+
{
|
|
1008
|
+
key: 'r1',
|
|
1009
|
+
title: 'Hide menu item',
|
|
1010
|
+
enable: true,
|
|
1011
|
+
condition: { logic: '$and', items: [] },
|
|
1012
|
+
actions: [{ key: 'a1', name: 'linkageSetMenuItemProps', params: { value: 'hidden' } }],
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
await model.saveStepParams();
|
|
1018
|
+
|
|
1019
|
+
expect(save).toHaveBeenCalledWith(model, { onlyStepParams: true });
|
|
1020
|
+
expect(updateRoute).toHaveBeenCalledWith(1, {
|
|
1021
|
+
options: {
|
|
1022
|
+
hasPersistedMenuInstanceFlow: true,
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
|
|
953
1027
|
it('should clear persisted menu linkage rules when no persisted state remains', async () => {
|
|
954
1028
|
const saveModel = vi.spyOn(engine, 'saveModel').mockResolvedValue(undefined as any);
|
|
955
1029
|
const destroy = vi.fn().mockResolvedValue(true);
|
|
@@ -23,6 +23,17 @@ const app = {
|
|
|
23
23
|
},
|
|
24
24
|
} as any;
|
|
25
25
|
|
|
26
|
+
// These fixtures mount the modern client under the `v2` segment; tell the
|
|
27
|
+
// runtime-prefix helper so `isV2AdminRuntime` detects it (the server injects
|
|
28
|
+
// this in production).
|
|
29
|
+
beforeAll(() => {
|
|
30
|
+
(window as any).__nocobase_modern_client_prefix__ = 'v2';
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
delete (window as any).__nocobase_modern_client_prefix__;
|
|
35
|
+
});
|
|
36
|
+
|
|
26
37
|
describe('resolveAdminRouteRuntimeTarget', () => {
|
|
27
38
|
it('should resolve flowPage to v2 spa runtime target', () => {
|
|
28
39
|
expect(
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { getV2EffectiveBasePath } from '../../../authRedirect';
|
|
10
|
+
import { getModernClientPrefix, getV2EffectiveBasePath } from '../../../authRedirect';
|
|
11
11
|
import type { BaseApplication } from '../../../BaseApplication';
|
|
12
12
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
|
|
13
13
|
|
|
@@ -26,8 +26,6 @@ export type AdminRouteRuntimeTarget = {
|
|
|
26
26
|
reason: AdminRouteRuntimeTargetReason;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
const V2_PUBLIC_PATH_SUFFIX = '/v2/';
|
|
30
|
-
|
|
31
29
|
type LocationLike = {
|
|
32
30
|
pathname: string;
|
|
33
31
|
search?: string;
|
|
@@ -69,7 +67,7 @@ function normalizePublicPath(value = '/') {
|
|
|
69
67
|
}
|
|
70
68
|
|
|
71
69
|
export function isV2AdminRuntime(app?: ResolveAdminRouteRuntimeTargetOptions['app']) {
|
|
72
|
-
return !!app?.getPublicPath && normalizePublicPath(app.getPublicPath()).endsWith(
|
|
70
|
+
return !!app?.getPublicPath && normalizePublicPath(app.getPublicPath()).endsWith(`/${getModernClientPrefix()}/`);
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
export function toRouterNavigationPath(pathname: string, basename?: string) {
|
|
@@ -34,13 +34,41 @@ const defaultToolbar = [
|
|
|
34
34
|
'code',
|
|
35
35
|
'inline-code',
|
|
36
36
|
'upload',
|
|
37
|
-
'fullscreen',
|
|
38
37
|
];
|
|
39
38
|
|
|
40
39
|
const NAMESPACE = 'block-markdown';
|
|
41
40
|
|
|
42
41
|
const locales = ['en_US', 'fr_FR', 'pt_BR', 'ja_JP', 'ko_KR', 'ru_RU', 'sv_SE', 'zh_CN', 'zh_TW'];
|
|
43
42
|
|
|
43
|
+
type MarkdownToolbarItem =
|
|
44
|
+
| string
|
|
45
|
+
| {
|
|
46
|
+
name: string;
|
|
47
|
+
tipPosition?: string;
|
|
48
|
+
toolbar?: MarkdownToolbarItem[];
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function placeToolbarTooltipsBelow(toolbar: MarkdownToolbarItem[]) {
|
|
53
|
+
return toolbar.map((item) => {
|
|
54
|
+
if (typeof item === 'string') {
|
|
55
|
+
if (item === '|' || item === 'br') {
|
|
56
|
+
return item;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
name: item,
|
|
60
|
+
tipPosition: 's',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
...item,
|
|
66
|
+
tipPosition: 's',
|
|
67
|
+
toolbar: item.toolbar ? placeToolbarTooltipsBelow(item.toolbar) : item.toolbar,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
const Edit = (props) => {
|
|
45
73
|
const { disabled, onChange, value, fileCollection, toolbar, vditorRef } = props;
|
|
46
74
|
const flowCtx = useFlowContext();
|
|
@@ -73,7 +101,7 @@ const Edit = (props) => {
|
|
|
73
101
|
useEffect(() => {
|
|
74
102
|
if (!containerRef.current) return;
|
|
75
103
|
|
|
76
|
-
const toolbarConfig = toolbar ?? defaultToolbar;
|
|
104
|
+
const toolbarConfig = placeToolbarTooltipsBelow(toolbar ?? defaultToolbar);
|
|
77
105
|
const vditor = new Vditor(containerRef.current, {
|
|
78
106
|
value: value ?? '',
|
|
79
107
|
lang,
|
|
@@ -321,7 +349,7 @@ export const MarkdownWithContextSelector: React.FC<MarkdownWithContextSelectorPr
|
|
|
321
349
|
if (quoteFlag !== false) {
|
|
322
350
|
setInnerValue(value);
|
|
323
351
|
}
|
|
324
|
-
}, [value]);
|
|
352
|
+
}, [quoteFlag, value]);
|
|
325
353
|
const handleTextChange = useCallback(
|
|
326
354
|
(e) => {
|
|
327
355
|
const val = e ?? '';
|
|
@@ -375,7 +403,7 @@ export const MarkdownWithContextSelector: React.FC<MarkdownWithContextSelectorPr
|
|
|
375
403
|
editor.focus();
|
|
376
404
|
});
|
|
377
405
|
},
|
|
378
|
-
[
|
|
406
|
+
[onChange],
|
|
379
407
|
);
|
|
380
408
|
|
|
381
409
|
const handleVariableSelected = useCallback(
|
|
@@ -13,14 +13,24 @@ import { FilterGroup, LinkageFilterItem } from './filter';
|
|
|
13
13
|
import { evaluateConditions, FilterGroupType, removeInvalidFilterItems } from '@nocobase/utils/client';
|
|
14
14
|
|
|
15
15
|
export const ConditionBuilder = observer(
|
|
16
|
-
(props: {
|
|
16
|
+
(props: {
|
|
17
|
+
value: FilterGroupType;
|
|
18
|
+
onChange: (value: FilterGroupType) => void;
|
|
19
|
+
extraMetaTree?: MetaTreeNode[];
|
|
20
|
+
maxAssociationFieldDepth?: number;
|
|
21
|
+
}) => {
|
|
17
22
|
const ctx = useFlowContext();
|
|
18
23
|
|
|
19
24
|
return (
|
|
20
25
|
<FilterGroup
|
|
21
26
|
value={props.value || { logic: '$and', items: [] }}
|
|
22
27
|
FilterItem={(itemProps) => (
|
|
23
|
-
<LinkageFilterItem
|
|
28
|
+
<LinkageFilterItem
|
|
29
|
+
model={ctx.model}
|
|
30
|
+
value={itemProps.value}
|
|
31
|
+
extraMetaTree={props.extraMetaTree}
|
|
32
|
+
maxAssociationFieldDepth={props.maxAssociationFieldDepth}
|
|
33
|
+
/>
|
|
24
34
|
)}
|
|
25
35
|
onChange={props.onChange}
|
|
26
36
|
/>
|