@nocobase/client-v2 2.2.0-alpha.8 → 2.2.0-alpha.9
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/APIClient.d.ts +1 -3
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +5 -1
- 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-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.mjs +103 -102
- package/lib/index.js +117 -116
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +7 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/__tests__/app.test.tsx +18 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +35 -1
- package/src/components/form/ScanInput/useCodeScanner.ts +16 -3
- 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 +86 -11
- package/src/flow/components/filter/VariableFilterItem.tsx +20 -5
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +49 -1
- 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 +184 -3
- 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 +572 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -1
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
|
@@ -0,0 +1,114 @@
|
|
|
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 { Tabs } from 'antd';
|
|
11
|
+
import type { TabsProps } from 'antd';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
|
|
14
|
+
export const NO_ACTIVE_PAGE_TAB_KEY = '__no_active_page_tab__';
|
|
15
|
+
|
|
16
|
+
type RenderTabBar = NonNullable<React.ComponentProps<typeof Tabs>['renderTabBar']>;
|
|
17
|
+
type PageTabItem = NonNullable<TabsProps['items']>[number];
|
|
18
|
+
|
|
19
|
+
type FilteredPageTabBarProps = {
|
|
20
|
+
hiddenTabKeys: Set<string>;
|
|
21
|
+
hiddenActiveTabLabel?: React.ReactNode;
|
|
22
|
+
items: PageTabItem[];
|
|
23
|
+
tabBarProps: Parameters<RenderTabBar>[0];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function FilteredPageTabBar({
|
|
27
|
+
hiddenTabKeys,
|
|
28
|
+
hiddenActiveTabLabel,
|
|
29
|
+
items,
|
|
30
|
+
tabBarProps,
|
|
31
|
+
}: FilteredPageTabBarProps) {
|
|
32
|
+
// Keep the outer Tabs instance responsible for every pane, while a navigation-only Tabs instance
|
|
33
|
+
// receives only visible items. This avoids relying on the rc-tabs Context instance bundled by antd.
|
|
34
|
+
const visibleItems = items
|
|
35
|
+
.filter((item) => !hiddenTabKeys.has(String(item.key)))
|
|
36
|
+
.map(
|
|
37
|
+
({
|
|
38
|
+
children: _children,
|
|
39
|
+
className: _className,
|
|
40
|
+
destroyInactiveTabPane: _destroyInactiveTabPane,
|
|
41
|
+
forceRender: _forceRender,
|
|
42
|
+
style: _style,
|
|
43
|
+
...item
|
|
44
|
+
}) => ({
|
|
45
|
+
...item,
|
|
46
|
+
children: null,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
const activeKey = visibleItems.some((item) => item.key === tabBarProps.activeKey)
|
|
50
|
+
? tabBarProps.activeKey
|
|
51
|
+
: NO_ACTIVE_PAGE_TAB_KEY;
|
|
52
|
+
const firstFocusableTabKey = visibleItems.find((item) => !item.disabled)?.key;
|
|
53
|
+
const hiddenActiveTabKey = hiddenTabKeys.has(String(tabBarProps.activeKey))
|
|
54
|
+
? String(tabBarProps.activeKey)
|
|
55
|
+
: undefined;
|
|
56
|
+
const hiddenActiveTabLabelId =
|
|
57
|
+
hiddenActiveTabKey && tabBarProps.id ? `${tabBarProps.id}-tab-${hiddenActiveTabKey}` : undefined;
|
|
58
|
+
const renderTabNode = (node: React.ReactElement) => {
|
|
59
|
+
const tabKey = String(node.key);
|
|
60
|
+
if (activeKey !== NO_ACTIVE_PAGE_TAB_KEY || tabKey !== String(firstFocusableTabKey)) {
|
|
61
|
+
return node;
|
|
62
|
+
}
|
|
63
|
+
let tabButtonFound = false;
|
|
64
|
+
const children = React.Children.map(
|
|
65
|
+
(node.props as { children?: React.ReactNode }).children,
|
|
66
|
+
(child: React.ReactNode) => {
|
|
67
|
+
if (tabButtonFound || !React.isValidElement(child)) {
|
|
68
|
+
return child;
|
|
69
|
+
}
|
|
70
|
+
const childProps = child.props as {
|
|
71
|
+
role?: string;
|
|
72
|
+
tabIndex?: number | null;
|
|
73
|
+
};
|
|
74
|
+
if (childProps.role !== 'tab') {
|
|
75
|
+
return child;
|
|
76
|
+
}
|
|
77
|
+
tabButtonFound = true;
|
|
78
|
+
return React.cloneElement(child as React.ReactElement<typeof childProps>, {
|
|
79
|
+
tabIndex: 0,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
return React.cloneElement(node, undefined, children);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
{hiddenActiveTabLabelId ? (
|
|
89
|
+
<span id={hiddenActiveTabLabelId} hidden>
|
|
90
|
+
{hiddenActiveTabLabel || hiddenActiveTabKey}
|
|
91
|
+
</span>
|
|
92
|
+
) : null}
|
|
93
|
+
<Tabs
|
|
94
|
+
activeKey={NO_ACTIVE_PAGE_TAB_KEY}
|
|
95
|
+
animated={{ inkBar: tabBarProps.animated.inkBar, tabPane: false }}
|
|
96
|
+
destroyInactiveTabPane
|
|
97
|
+
id={tabBarProps.id}
|
|
98
|
+
items={visibleItems}
|
|
99
|
+
more={tabBarProps.more}
|
|
100
|
+
onTabClick={(key, event) => tabBarProps.onTabClick(key, event)}
|
|
101
|
+
onTabScroll={tabBarProps.onTabScroll}
|
|
102
|
+
renderTabBar={(navigationTabBarProps, NavigationDefaultTabBar) => (
|
|
103
|
+
<NavigationDefaultTabBar {...navigationTabBarProps} activeKey={activeKey}>
|
|
104
|
+
{renderTabNode}
|
|
105
|
+
</NavigationDefaultTabBar>
|
|
106
|
+
)}
|
|
107
|
+
tabBarExtraContent={tabBarProps.extra}
|
|
108
|
+
tabBarGutter={tabBarProps.tabBarGutter}
|
|
109
|
+
tabBarStyle={tabBarProps.style}
|
|
110
|
+
tabPosition={tabBarProps.tabPosition}
|
|
111
|
+
/>
|
|
112
|
+
</>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { FlowModel, FlowModelRenderer, observable, tExpr } from '@nocobase/flow-engine';
|
|
10
|
+
import { FlowModel, FlowModelRenderer, observable, type ParamObject, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import { Icon, type NocoBaseDesktopRoute } from '../../../../flow-compat';
|
|
12
12
|
import { useRequest } from 'ahooks';
|
|
13
|
+
import _ from 'lodash';
|
|
13
14
|
import React from 'react';
|
|
14
15
|
import { SkeletonFallback } from '../../../components/SkeletonFallback';
|
|
15
16
|
import { TextAreaWithContextSelector } from '../../../components/TextAreaWithContextSelector';
|
|
@@ -52,6 +53,24 @@ function normalizePersistedRoute(payload: unknown): Partial<NocoBaseDesktopRoute
|
|
|
52
53
|
return undefined;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
type PersistedFlowModelAnchor = Record<string, unknown> & {
|
|
57
|
+
uid?: unknown;
|
|
58
|
+
stepParams?: unknown;
|
|
59
|
+
subModels?: unknown;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type LinkageRulesStepParams = ParamObject & {
|
|
63
|
+
value?: unknown;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
67
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isLinkageRulesStepParams(value: unknown): value is LinkageRulesStepParams {
|
|
71
|
+
return isRecord(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
export class BasePageTabModel extends FlowModel<{
|
|
56
75
|
subModels: {
|
|
57
76
|
grid: BlockGridModel;
|
|
@@ -139,13 +158,100 @@ BasePageTabModel.registerFlow({
|
|
|
139
158
|
ctx.model.setProps('title', translate(params.title, { ns: 'lm-desktop-routes' }));
|
|
140
159
|
ctx.model.setProps('icon', params.icon);
|
|
141
160
|
const pageModel = ctx.engine.getModel(ctx.model.parentId) as { updateDocumentTitle?: () => Promise<void> };
|
|
142
|
-
|
|
161
|
+
await pageModel?.updateDocumentTitle?.();
|
|
143
162
|
},
|
|
144
163
|
},
|
|
164
|
+
linkageRules: {
|
|
165
|
+
use: 'tabLinkageRules',
|
|
166
|
+
},
|
|
145
167
|
},
|
|
146
168
|
});
|
|
147
169
|
|
|
148
170
|
export class RootPageTabModel extends BasePageTabModel {
|
|
171
|
+
private persistedLinkageRulesHydrated = false;
|
|
172
|
+
private persistedLinkageRulesHydrating?: Promise<void>;
|
|
173
|
+
|
|
174
|
+
onInit(options) {
|
|
175
|
+
super.onInit(options);
|
|
176
|
+
if (this.shouldHydratePersistedLinkageRules()) {
|
|
177
|
+
this.hydratePersistedLinkageRules().catch((error) => {
|
|
178
|
+
console.warn('[RootPageTabModel] Failed to hydrate tab linkage rules', error);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private hasExplicitLinkageRulesStep() {
|
|
184
|
+
const pageTabSettings = this.stepParams?.pageTabSettings;
|
|
185
|
+
return isRecord(pageTabSettings) && Object.prototype.hasOwnProperty.call(pageTabSettings, 'linkageRules');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private shouldHydratePersistedLinkageRules() {
|
|
189
|
+
return !!this.context.flowSettingsEnabled || this.props.route?.options?.hasPersistedPageTabFlowModel === true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async fetchPersistedAnchor(): Promise<PersistedFlowModelAnchor | undefined> {
|
|
193
|
+
const response = await this.context.api.request({
|
|
194
|
+
url: 'flowModels:findOne',
|
|
195
|
+
params: { uid: this.uid },
|
|
196
|
+
});
|
|
197
|
+
const anchor = response?.data?.data;
|
|
198
|
+
return isRecord(anchor) ? anchor : undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async performPersistedLinkageRulesHydrate() {
|
|
202
|
+
if (this.hasExplicitLinkageRulesStep()) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const anchor = await this.fetchPersistedAnchor();
|
|
207
|
+
if (this.hasExplicitLinkageRulesStep()) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const stepParams = isRecord(anchor?.stepParams) ? anchor.stepParams : undefined;
|
|
212
|
+
const pageTabSettings = isRecord(stepParams?.pageTabSettings) ? stepParams.pageTabSettings : undefined;
|
|
213
|
+
const linkageRules = pageTabSettings?.linkageRules;
|
|
214
|
+
if (!isLinkageRulesStepParams(linkageRules)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.setStepParams('pageTabSettings', 'linkageRules', _.cloneDeep(linkageRules));
|
|
219
|
+
this.invalidateFlowCache('beforeRender', true);
|
|
220
|
+
await this.rerender();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private hydratePersistedLinkageRules(): Promise<void> {
|
|
224
|
+
if (this.persistedLinkageRulesHydrated) {
|
|
225
|
+
return Promise.resolve();
|
|
226
|
+
}
|
|
227
|
+
if (this.persistedLinkageRulesHydrating) {
|
|
228
|
+
return this.persistedLinkageRulesHydrating;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const hydration = this.performPersistedLinkageRulesHydrate();
|
|
232
|
+
this.persistedLinkageRulesHydrating = hydration;
|
|
233
|
+
hydration
|
|
234
|
+
.then(() => {
|
|
235
|
+
if (this.persistedLinkageRulesHydrating === hydration) {
|
|
236
|
+
this.persistedLinkageRulesHydrated = true;
|
|
237
|
+
this.persistedLinkageRulesHydrating = undefined;
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
.catch(() => {
|
|
241
|
+
if (this.persistedLinkageRulesHydrating === hydration) {
|
|
242
|
+
this.persistedLinkageRulesHydrating = undefined;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
return hydration;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async openFlowSettings(options?: Parameters<FlowModel['openFlowSettings']>[0]) {
|
|
249
|
+
if (options?.flowKey === 'pageTabSettings' && options?.stepKey === 'linkageRules') {
|
|
250
|
+
await this.hydratePersistedLinkageRules();
|
|
251
|
+
}
|
|
252
|
+
return super.openFlowSettings(options);
|
|
253
|
+
}
|
|
254
|
+
|
|
149
255
|
renderChildren() {
|
|
150
256
|
return (
|
|
151
257
|
<PageTabChildrenRenderer
|
|
@@ -162,7 +268,15 @@ export class RootPageTabModel extends BasePageTabModel {
|
|
|
162
268
|
}
|
|
163
269
|
|
|
164
270
|
async saveStepParams() {
|
|
165
|
-
|
|
271
|
+
const hasExplicitLinkageRulesStep = this.hasExplicitLinkageRulesStep();
|
|
272
|
+
const linkageRules = hasExplicitLinkageRulesStep
|
|
273
|
+
? (_.cloneDeep(this.stepParams.pageTabSettings.linkageRules) as LinkageRulesStepParams)
|
|
274
|
+
: undefined;
|
|
275
|
+
|
|
276
|
+
await this.save();
|
|
277
|
+
if (hasExplicitLinkageRulesStep && linkageRules) {
|
|
278
|
+
await this.persistLinkageRulesToAnchor(linkageRules);
|
|
279
|
+
}
|
|
166
280
|
}
|
|
167
281
|
|
|
168
282
|
async save() {
|
|
@@ -179,6 +293,7 @@ export class RootPageTabModel extends BasePageTabModel {
|
|
|
179
293
|
title: this.getTabTitle(''),
|
|
180
294
|
icon: this.getTabIcon(),
|
|
181
295
|
options: {
|
|
296
|
+
...(this.props.route?.options || {}),
|
|
182
297
|
flowRegistry: json.flowRegistry,
|
|
183
298
|
documentTitle,
|
|
184
299
|
},
|
|
@@ -199,6 +314,72 @@ export class RootPageTabModel extends BasePageTabModel {
|
|
|
199
314
|
}
|
|
200
315
|
}
|
|
201
316
|
|
|
317
|
+
private buildAnchorPayload(latestAnchor: PersistedFlowModelAnchor | undefined, linkageRules: LinkageRulesStepParams) {
|
|
318
|
+
const hasPersistedAnchor = typeof latestAnchor?.uid === 'string' && latestAnchor.uid.length > 0;
|
|
319
|
+
const anchorRoot: PersistedFlowModelAnchor = hasPersistedAnchor
|
|
320
|
+
? _.cloneDeep(latestAnchor)
|
|
321
|
+
: { uid: this.uid, use: 'RouteModel' };
|
|
322
|
+
delete anchorRoot.subModels;
|
|
323
|
+
|
|
324
|
+
const latestStepParams = isRecord(anchorRoot.stepParams) ? anchorRoot.stepParams : {};
|
|
325
|
+
const latestPageTabSettings = isRecord(latestStepParams.pageTabSettings) ? latestStepParams.pageTabSettings : {};
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
...anchorRoot,
|
|
329
|
+
stepParams: {
|
|
330
|
+
...latestStepParams,
|
|
331
|
+
pageTabSettings: {
|
|
332
|
+
...latestPageTabSettings,
|
|
333
|
+
linkageRules: _.cloneDeep(linkageRules),
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private async persistLinkageRulesToAnchor(linkageRules: LinkageRulesStepParams) {
|
|
340
|
+
const latestAnchor = await this.fetchPersistedAnchor();
|
|
341
|
+
const anchorPayload = this.buildAnchorPayload(latestAnchor, linkageRules);
|
|
342
|
+
await this.context.api.request({
|
|
343
|
+
method: 'post',
|
|
344
|
+
url: 'flowModels:save',
|
|
345
|
+
data: anchorPayload,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const hasRules = Array.isArray(linkageRules.value) && linkageRules.value.length > 0;
|
|
349
|
+
await this.syncPersistedPageTabFlowModelMarker(hasRules);
|
|
350
|
+
this.persistedLinkageRulesHydrated = true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private async syncPersistedPageTabFlowModelMarker(hasRules: boolean) {
|
|
354
|
+
const route = this.props.route;
|
|
355
|
+
if (route?.id == null) {
|
|
356
|
+
throw new Error('Cannot persist page tab FlowModel marker before the desktop route is saved.');
|
|
357
|
+
}
|
|
358
|
+
if (typeof this.context.routeRepository?.updateRoute !== 'function') {
|
|
359
|
+
throw new Error('Route repository is unavailable while persisting the page tab FlowModel marker.');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const nextOptions = {
|
|
363
|
+
...(route.options || {}),
|
|
364
|
+
};
|
|
365
|
+
if (hasRules) {
|
|
366
|
+
nextOptions.hasPersistedPageTabFlowModel = true;
|
|
367
|
+
} else {
|
|
368
|
+
delete nextOptions.hasPersistedPageTabFlowModel;
|
|
369
|
+
}
|
|
370
|
+
const persistedOptions = Object.keys(nextOptions).length > 0 ? nextOptions : undefined;
|
|
371
|
+
|
|
372
|
+
await this.context.routeRepository.updateRoute(
|
|
373
|
+
route.id,
|
|
374
|
+
{ options: persistedOptions },
|
|
375
|
+
{ refreshAfterMutation: false },
|
|
376
|
+
);
|
|
377
|
+
this.setProps('route', {
|
|
378
|
+
...route,
|
|
379
|
+
options: persistedOptions,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
202
383
|
async destroy() {
|
|
203
384
|
this.observerDispose();
|
|
204
385
|
this.invalidateFlowCache('beforeRender', true);
|