@nocobase/client-v2 2.1.0-beta.34 → 2.1.0-beta.36
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 +7 -1
- package/es/PluginManager.d.ts +2 -0
- package/es/components/PoweredBy.d.ts +18 -0
- package/es/components/SwitchLanguage.d.ts +11 -0
- package/es/components/form/DialogFormLayout.d.ts +75 -0
- package/es/components/form/DrawerFormLayout.d.ts +11 -11
- package/es/components/form/PasswordInput.d.ts +40 -0
- package/es/components/form/RemoteSelect.d.ts +79 -0
- package/es/components/form/index.d.ts +3 -0
- package/es/components/form/table/styles.d.ts +10 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/models/base/ActionModelCore.d.ts +6 -0
- package/es/flow/models/base/GridModel.d.ts +2 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
- package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/hooks/index.d.ts +2 -0
- package/es/hooks/useCurrentAppInfo.d.ts +9 -0
- package/es/index.mjs +117 -105
- package/es/json-logic/globalOperators.d.ts +11 -0
- package/es/nocobase-buildin-plugin/index.d.ts +25 -0
- package/es/utils/appVersionHTML.d.ts +10 -0
- package/es/utils/globalDeps.d.ts +7 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/remotePlugins.d.ts +4 -1
- package/lib/index.js +120 -108
- package/package.json +7 -6
- package/src/BaseApplication.tsx +11 -3
- package/src/PluginManager.ts +2 -0
- package/src/PluginSettingsManager.ts +2 -1
- package/src/__tests__/PluginSettingsManager.test.ts +19 -0
- package/src/__tests__/PoweredBy.test.tsx +130 -0
- package/src/__tests__/app.test.tsx +39 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
- package/src/__tests__/remotePlugins.test.ts +203 -0
- package/src/__tests__/useCurrentRoles.test.tsx +100 -0
- package/src/components/PoweredBy.tsx +71 -0
- package/src/components/README.md +314 -0
- package/src/components/README.zh-CN.md +312 -0
- package/src/components/SwitchLanguage.tsx +48 -0
- package/src/components/form/DialogFormLayout.tsx +111 -0
- package/src/components/form/DrawerFormLayout.tsx +13 -32
- package/src/components/form/PasswordInput.tsx +211 -0
- package/src/components/form/RemoteSelect.tsx +137 -0
- package/src/components/form/index.tsx +3 -0
- package/src/components/form/table/Table.tsx +2 -1
- package/src/components/form/table/styles.ts +19 -0
- package/src/components/index.ts +2 -0
- package/src/css-variable/CSSVariableProvider.tsx +10 -1
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
- package/src/flow/actions/dataScope.tsx +3 -0
- package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/models/base/ActionModel.tsx +8 -7
- package/src/flow/models/base/ActionModelCore.tsx +15 -7
- package/src/flow/models/base/GridModel.tsx +93 -36
- package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
- package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useCurrentAppInfo.ts +36 -0
- package/src/json-logic/globalOperators.js +731 -0
- package/src/nocobase-buildin-plugin/index.tsx +70 -16
- package/src/utils/appVersionHTML.ts +28 -0
- package/src/utils/globalDeps.ts +47 -31
- package/src/utils/index.tsx +2 -0
- package/src/utils/remotePlugins.ts +119 -13
|
@@ -0,0 +1,96 @@
|
|
|
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 { EventEmitter } from 'events';
|
|
12
|
+
import { ensureFormValueDrivenDataScopeClear } from '../../utils/dataScopeFormValueClear';
|
|
13
|
+
|
|
14
|
+
describe('ensureFormValueDrivenDataScopeClear', () => {
|
|
15
|
+
it('clears field value when referenced formValues dependency changes', () => {
|
|
16
|
+
const emitter = new EventEmitter();
|
|
17
|
+
const formBlock = {
|
|
18
|
+
uid: 'form-1',
|
|
19
|
+
disposed: false,
|
|
20
|
+
emitter,
|
|
21
|
+
context: { form: {} },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const onChange = vi.fn();
|
|
25
|
+
const model: any = {
|
|
26
|
+
disposed: false,
|
|
27
|
+
props: {
|
|
28
|
+
value: { id: 1 },
|
|
29
|
+
onChange,
|
|
30
|
+
},
|
|
31
|
+
context: {
|
|
32
|
+
blockModel: formBlock,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ctx: any = {
|
|
37
|
+
model,
|
|
38
|
+
flowKey: 'selectSettings',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const filter = {
|
|
42
|
+
logic: '$and',
|
|
43
|
+
items: [{ path: 'schoolId', operator: '$eq', value: '{{ ctx.formValues.school.id }}' }],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
ensureFormValueDrivenDataScopeClear(ctx, filter);
|
|
47
|
+
|
|
48
|
+
emitter.emit('formValuesChange', {
|
|
49
|
+
changedValues: { school: { id: 2 } },
|
|
50
|
+
allValues: { school: { id: 2 }, class: { id: 1 } },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not clear when dependency did not change', () => {
|
|
57
|
+
const emitter = new EventEmitter();
|
|
58
|
+
const formBlock = {
|
|
59
|
+
uid: 'form-1',
|
|
60
|
+
disposed: false,
|
|
61
|
+
emitter,
|
|
62
|
+
context: { form: {} },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onChange = vi.fn();
|
|
66
|
+
const model: any = {
|
|
67
|
+
disposed: false,
|
|
68
|
+
props: {
|
|
69
|
+
value: { id: 1 },
|
|
70
|
+
onChange,
|
|
71
|
+
},
|
|
72
|
+
context: {
|
|
73
|
+
blockModel: formBlock,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ctx: any = {
|
|
78
|
+
model,
|
|
79
|
+
flowKey: 'selectSettings',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const filter = {
|
|
83
|
+
logic: '$and',
|
|
84
|
+
items: [{ path: 'schoolId', operator: '$eq', value: '{{ ctx.formValues.school.id }}' }],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
ensureFormValueDrivenDataScopeClear(ctx, filter);
|
|
88
|
+
|
|
89
|
+
emitter.emit('formValuesChange', {
|
|
90
|
+
changedValues: { class: null },
|
|
91
|
+
allValues: { school: { id: 2 }, class: null },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -13,6 +13,7 @@ import React from 'react';
|
|
|
13
13
|
import { FilterGroup, VariableFilterItem } from '../components/filter';
|
|
14
14
|
import { FieldModel } from '../models/base/FieldModel';
|
|
15
15
|
import { normalizeDataScopeFilter } from './dataScopeFilter';
|
|
16
|
+
import { ensureFormValueDrivenDataScopeClear } from '../utils/dataScopeFormValueClear';
|
|
16
17
|
|
|
17
18
|
export const dataScope = defineAction({
|
|
18
19
|
name: 'dataScope',
|
|
@@ -54,6 +55,8 @@ export const dataScope = defineAction({
|
|
|
54
55
|
const resolvedFilter = await ctx.resolveJsonTemplate(params.filter);
|
|
55
56
|
const filter = normalizeDataScopeFilter(params.filter, resolvedFilter);
|
|
56
57
|
|
|
58
|
+
ensureFormValueDrivenDataScopeClear(ctx as any, params.filter);
|
|
59
|
+
|
|
57
60
|
if (isEmptyFilter(filter)) {
|
|
58
61
|
resource.removeFilterGroup(ctx.model.uid);
|
|
59
62
|
} else {
|
|
@@ -122,12 +122,11 @@ const FilterFormDefaultValuesUI = observer(
|
|
|
122
122
|
rootCollection={getCollectionFromModel(ctx.model)}
|
|
123
123
|
value={value}
|
|
124
124
|
onChange={handleChange}
|
|
125
|
-
fixedMode="default"
|
|
126
|
-
showCondition={false}
|
|
127
125
|
showValueEditorWhenNoField
|
|
128
126
|
getValueInputProps={getValueInputProps}
|
|
129
127
|
isTitleFieldCandidate={isTitleFieldCandidate}
|
|
130
128
|
onSyncAssociationTitleField={onSyncAssociationTitleField}
|
|
129
|
+
enableDateVariableAsConstant
|
|
131
130
|
/>
|
|
132
131
|
);
|
|
133
132
|
},
|
|
@@ -371,15 +371,12 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
371
371
|
const [allAccessRoutes, setAllAccessRoutes] = useState<NocoBaseDesktopRoute[]>(
|
|
372
372
|
() => flowEngine.context.routeRepository?.listAccessible?.() || [],
|
|
373
373
|
);
|
|
374
|
-
const screens = Grid.useBreakpoint();
|
|
375
|
-
const isMobileViewport =
|
|
376
|
-
screens.md === false || (screens.md === undefined && typeof window !== 'undefined' && window.innerWidth < 768);
|
|
377
374
|
const location = useLocation();
|
|
378
375
|
const { token } = antdTheme.useToken();
|
|
379
376
|
const customToken = token as CustomToken;
|
|
380
377
|
const isMobileLayout = !!adminLayoutModel?.isMobileLayout;
|
|
381
378
|
const menuRouteRefreshVersion = adminLayoutModel?.menuRouteRefreshVersion || 0;
|
|
382
|
-
const isMobileSider = isMobileLayout
|
|
379
|
+
const isMobileSider = isMobileLayout;
|
|
383
380
|
const [collapsed, setCollapsed] = useState(isMobileSider);
|
|
384
381
|
const [preferredFlowSettingsEnabled, setPreferredFlowSettingsEnabled] = useState(() => readFlowSettingsPreference());
|
|
385
382
|
const [route, setRoute] = useState<{ path: string; children: AdminLayoutMenuNode[] }>({
|
|
@@ -9,45 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
|
11
11
|
import { css } from '@emotion/css';
|
|
12
|
-
import { observer
|
|
12
|
+
import { observer } from '@nocobase/flow-engine';
|
|
13
13
|
import { parseHTML } from '@nocobase/utils/client';
|
|
14
14
|
import { Dropdown, Menu, Popover, theme as antdTheme } from 'antd';
|
|
15
15
|
import type { MenuItemType, MenuDividerType } from 'antd/es/menu/interface';
|
|
16
|
-
import React, {
|
|
16
|
+
import React, { useMemo, useState } from 'react';
|
|
17
17
|
import { useTranslation } from 'react-i18next';
|
|
18
18
|
import { usePlugin } from '../../../flow-compat';
|
|
19
|
+
import { useCurrentAppInfo } from '../../../hooks';
|
|
19
20
|
import type { CustomToken } from '../../../theme';
|
|
21
|
+
import { getAppVersionHTML } from '../../../utils';
|
|
20
22
|
|
|
21
23
|
type SettingsMenuItemType = MenuItemType | MenuDividerType;
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
* 读取当前应用信息,避免继续依赖旧的 CurrentAppInfoProvider。
|
|
25
|
-
*/
|
|
26
|
-
function useCurrentAppInfoLite() {
|
|
27
|
-
const flowEngine = useFlowEngine();
|
|
28
|
-
const [data, setData] = useState<any>();
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
let active = true;
|
|
32
|
-
|
|
33
|
-
Promise.resolve(flowEngine.context.appInfo)
|
|
34
|
-
.then((info) => {
|
|
35
|
-
if (active) {
|
|
36
|
-
setData(info);
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
.catch((error) => {
|
|
40
|
-
console.error(error);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return () => {
|
|
44
|
-
active = false;
|
|
45
|
-
};
|
|
46
|
-
}, [flowEngine]);
|
|
47
|
-
|
|
48
|
-
return data;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
25
|
const helpClassName = css`
|
|
52
26
|
display: inline-block;
|
|
53
27
|
vertical-align: top;
|
|
@@ -60,7 +34,7 @@ const helpClassName = css`
|
|
|
60
34
|
|
|
61
35
|
const SettingsMenu: React.FC = () => {
|
|
62
36
|
const { t } = useTranslation();
|
|
63
|
-
const appInfo =
|
|
37
|
+
const appInfo = useCurrentAppInfo();
|
|
64
38
|
const { token } = antdTheme.useToken();
|
|
65
39
|
const isSimplifiedChinese = appInfo?.lang === 'zh-CN';
|
|
66
40
|
|
|
@@ -136,7 +110,7 @@ export const HelpLite = observer(
|
|
|
136
110
|
const { token } = antdTheme.useToken();
|
|
137
111
|
const customToken = token as CustomToken;
|
|
138
112
|
const customBrandPlugin: any = usePlugin('@nocobase/plugin-custom-brand');
|
|
139
|
-
const appInfo =
|
|
113
|
+
const appInfo = useCurrentAppInfo();
|
|
140
114
|
|
|
141
115
|
const icon = (
|
|
142
116
|
<span
|
|
@@ -156,7 +130,7 @@ export const HelpLite = observer(
|
|
|
156
130
|
);
|
|
157
131
|
|
|
158
132
|
if (customBrandPlugin?.options?.options?.about) {
|
|
159
|
-
const appVersion =
|
|
133
|
+
const appVersion = getAppVersionHTML(appInfo?.version);
|
|
160
134
|
const content = parseHTML(customBrandPlugin.options.options.about, { appVersion });
|
|
161
135
|
|
|
162
136
|
return (
|
|
@@ -91,7 +91,7 @@ const useBlockHeight = ({
|
|
|
91
91
|
const padding = getPadding(root);
|
|
92
92
|
const addBlockContainer = getAddBlockContainer(root);
|
|
93
93
|
const pageTop = rootRect.top + padding.top;
|
|
94
|
-
const topOffset = Math.
|
|
94
|
+
const topOffset = Math.max(0, cardRect.top - pageTop);
|
|
95
95
|
let bottomOffset = padding.bottom + ctx.themeToken.marginBlock;
|
|
96
96
|
if (addBlockContainer) {
|
|
97
97
|
const gapBetween = ctx.themeToken.marginBlock;
|
|
@@ -99,7 +99,7 @@ const useBlockHeight = ({
|
|
|
99
99
|
}
|
|
100
100
|
const nextHeight = Math.max(
|
|
101
101
|
0,
|
|
102
|
-
Math.floor(window.innerHeight - getValidPageTop(pageTop, 110) - topOffset - bottomOffset),
|
|
102
|
+
Math.floor(window.innerHeight - getValidPageTop(pageTop, 110) - topOffset - bottomOffset - 1),
|
|
103
103
|
);
|
|
104
104
|
setFullHeight((prev) => (prev === nextHeight ? prev : nextHeight));
|
|
105
105
|
}, [heightMode, cardRef]);
|
|
@@ -48,18 +48,19 @@ ActionModel.registerFlow({
|
|
|
48
48
|
title: tExpr('Button icon'),
|
|
49
49
|
}
|
|
50
50
|
: undefined,
|
|
51
|
+
iconOnly: ctx.model.enableEditIcon
|
|
52
|
+
? {
|
|
53
|
+
'x-decorator': 'FormItem',
|
|
54
|
+
'x-component': 'Switch',
|
|
55
|
+
title: tExpr('Icon only'),
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
51
58
|
type: ctx.model.enableEditType
|
|
52
59
|
? {
|
|
53
60
|
'x-decorator': 'FormItem',
|
|
54
61
|
'x-component': 'Radio.Group',
|
|
55
62
|
title: tExpr('Button type'),
|
|
56
|
-
enum:
|
|
57
|
-
{ value: 'default', label: '{{t("Default")}}' },
|
|
58
|
-
{ value: 'primary', label: '{{t("Primary")}}' },
|
|
59
|
-
{ value: 'dashed', label: '{{t("Dashed")}}' },
|
|
60
|
-
{ value: 'link', label: '{{t("Link")}}' },
|
|
61
|
-
{ value: 'text', label: '{{t("Text")}}' },
|
|
62
|
-
],
|
|
63
|
+
enum: ctx.model.buttonTypeOptions,
|
|
63
64
|
}
|
|
64
65
|
: undefined,
|
|
65
66
|
danger: ctx.model.enableEditDanger
|
|
@@ -50,12 +50,13 @@ export const ActionSceneEnum = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export class ActionModel<T extends DefaultStructure = DefaultStructure> extends FlowModel<T> {
|
|
53
|
-
declare props: ButtonProps & { tooltip?: string };
|
|
53
|
+
declare props: ButtonProps & { tooltip?: string; iconOnly?: boolean };
|
|
54
54
|
declare scene: ActionSceneType;
|
|
55
55
|
|
|
56
|
-
defaultProps: ButtonProps & { tooltip?: string } = {
|
|
56
|
+
defaultProps: ButtonProps & { tooltip?: string; iconOnly?: boolean } = {
|
|
57
57
|
type: 'default',
|
|
58
58
|
title: tExpr('Action'),
|
|
59
|
+
iconOnly: false,
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
enableEditTooltip = true;
|
|
@@ -64,6 +65,13 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
64
65
|
enableEditType = true;
|
|
65
66
|
enableEditDanger = true;
|
|
66
67
|
enableEditColor = false;
|
|
68
|
+
buttonTypeOptions = [
|
|
69
|
+
{ value: 'default', label: '{{t("Default")}}' },
|
|
70
|
+
{ value: 'primary', label: '{{t("Primary")}}' },
|
|
71
|
+
{ value: 'dashed', label: '{{t("Dashed")}}' },
|
|
72
|
+
{ value: 'link', label: '{{t("Link")}}' },
|
|
73
|
+
{ value: 'text', label: '{{t("Text")}}' },
|
|
74
|
+
];
|
|
67
75
|
|
|
68
76
|
static _getScene() {
|
|
69
77
|
return _.castArray(this['scene'] || []);
|
|
@@ -133,12 +141,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
renderButton() {
|
|
136
|
-
const props = this.props;
|
|
144
|
+
const { iconOnly, ...props } = this.props;
|
|
137
145
|
const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
|
|
138
146
|
|
|
139
147
|
return (
|
|
140
148
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon}>
|
|
141
|
-
{props.children || this.getTitle()}
|
|
149
|
+
{iconOnly ? null : props.children || this.getTitle()}
|
|
142
150
|
</Button>
|
|
143
151
|
);
|
|
144
152
|
}
|
|
@@ -152,13 +160,13 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
renderHiddenInConfig(): React.ReactNode | undefined {
|
|
155
|
-
const props = this.props;
|
|
163
|
+
const { iconOnly, ...props } = this.props;
|
|
156
164
|
const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
|
|
157
165
|
if (this.forbidden) {
|
|
158
166
|
return (
|
|
159
167
|
<ActionWithoutPermission>
|
|
160
168
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
|
|
161
|
-
{props.children || this.getTitle()}
|
|
169
|
+
{iconOnly ? null : props.children || this.getTitle()}
|
|
162
170
|
</Button>
|
|
163
171
|
</ActionWithoutPermission>
|
|
164
172
|
);
|
|
@@ -166,7 +174,7 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
166
174
|
return (
|
|
167
175
|
<Tooltip title={this.context.t('The button is hidden and only visible when the UI Editor is active')}>
|
|
168
176
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
|
|
169
|
-
{props.children || this.getTitle()}
|
|
177
|
+
{iconOnly ? null : props.children || this.getTitle()}
|
|
170
178
|
</Button>
|
|
171
179
|
</Tooltip>
|
|
172
180
|
);
|
|
@@ -359,6 +359,58 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
359
359
|
return rowElement?.parentElement?.clientWidth || fallbackWidth;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
private prunePlaceholderOnlyRows(layout: GridLayoutV2): GridLayoutV2 {
|
|
363
|
+
type RowCellEntry = {
|
|
364
|
+
cell: GridCellV2;
|
|
365
|
+
size: number;
|
|
366
|
+
hasRealItem: boolean;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const pruneRows = (rows: GridRowV2[]): GridRowV2[] => {
|
|
370
|
+
return rows
|
|
371
|
+
.map((row) => {
|
|
372
|
+
const cellsWithSizes = row.cells
|
|
373
|
+
.map((cell, index) => {
|
|
374
|
+
const size = row.sizes?.[index] ?? 1;
|
|
375
|
+
if (cell.rows?.length) {
|
|
376
|
+
const childRows = pruneRows(cell.rows);
|
|
377
|
+
if (childRows.length) {
|
|
378
|
+
return { cell: { ...cell, rows: childRows }, size, hasRealItem: true };
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const items = cell.items || [];
|
|
384
|
+
const hasRealItem = items.some((uid) => uid !== EMPTY_COLUMN_UID);
|
|
385
|
+
const hasEmptyPlaceholder = items.includes(EMPTY_COLUMN_UID);
|
|
386
|
+
if (hasRealItem || hasEmptyPlaceholder) {
|
|
387
|
+
return { cell, size, hasRealItem };
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
})
|
|
391
|
+
.filter(Boolean) as RowCellEntry[];
|
|
392
|
+
|
|
393
|
+
if (!cellsWithSizes.some((entry) => entry.hasRealItem)) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
...row,
|
|
399
|
+
cells: cellsWithSizes.map((entry) => entry.cell),
|
|
400
|
+
sizes: cellsWithSizes.map((entry) => entry.size),
|
|
401
|
+
};
|
|
402
|
+
})
|
|
403
|
+
.filter(Boolean) as GridRowV2[];
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
return normalizeGridLayout({
|
|
407
|
+
layout: { ...layout, rows: pruneRows(layout.rows || []) },
|
|
408
|
+
itemUids: this.getItemUids(),
|
|
409
|
+
gridUid: this.uid,
|
|
410
|
+
logger: console,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
362
414
|
private resizeGridLayout({
|
|
363
415
|
direction,
|
|
364
416
|
resizeDistance,
|
|
@@ -428,7 +480,9 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
428
480
|
this.emitter.on('onSubModelDestroyed', (model: FlowModel) => {
|
|
429
481
|
const modelUid = model.uid;
|
|
430
482
|
this.resetRows(true);
|
|
431
|
-
this.
|
|
483
|
+
const layout = this.prunePlaceholderOnlyRows(this.props.layout);
|
|
484
|
+
this.setGridStepLayout(layout);
|
|
485
|
+
this.syncLayoutProps(layout);
|
|
432
486
|
|
|
433
487
|
// 删除筛选配置
|
|
434
488
|
this.context.filterManager?.removeFilterConfig({ targetId: modelUid });
|
|
@@ -855,6 +909,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
855
909
|
* 运行态按可见 block 过滤行/列,避免“整行都是 hidden block”但依然保留行间距占位。
|
|
856
910
|
* - 配置态(flowSettingsEnabled)保持原始 rows/sizes 以便拖拽和布局编辑。
|
|
857
911
|
* - 运行态仅在判断为“整列/整行都不可见”时做过滤,不写回 props/stepParams,布局元数据保持不变。
|
|
912
|
+
* - 空列是拖拽缩窄后保存的布局占位,运行态需要保留其宽度,但 Grid 渲染层不会渲染其内容。
|
|
858
913
|
*/
|
|
859
914
|
private getVisibleLayout() {
|
|
860
915
|
const rawLayout = this.normalizeLayoutFromSource();
|
|
@@ -877,51 +932,53 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
877
932
|
}
|
|
878
933
|
|
|
879
934
|
const items = this.subModels?.items || [];
|
|
880
|
-
if (!items.length) {
|
|
881
|
-
return { layout: baseLayout, rows: baseProjection.rows, sizes: baseProjection.sizes };
|
|
882
|
-
}
|
|
883
|
-
|
|
884
935
|
const modelByUid = new Map(items.map((m: FlowModel) => [m.uid, m]));
|
|
936
|
+
type VisibleCellEntry = {
|
|
937
|
+
cell: GridLayoutV2['rows'][number]['cells'][number];
|
|
938
|
+
size: number;
|
|
939
|
+
hasVisibleContent: boolean;
|
|
940
|
+
};
|
|
885
941
|
|
|
886
942
|
const filterRows = (rows: GridLayoutV2['rows']): GridLayoutV2['rows'] => {
|
|
887
943
|
return rows
|
|
888
944
|
.map((row) => {
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
keptSizes.push(keepSize);
|
|
945
|
+
const cellsWithSizes = row.cells
|
|
946
|
+
.map((cell, index) => {
|
|
947
|
+
const sourceSize = row.sizes?.[index];
|
|
948
|
+
const keepSize = Number.isFinite(sourceSize) && sourceSize > 0 ? sourceSize : 1;
|
|
949
|
+
if (cell.rows) {
|
|
950
|
+
const childRows = filterRows(cell.rows);
|
|
951
|
+
if (childRows.length) {
|
|
952
|
+
return { cell: { ...cell, rows: childRows }, size: keepSize, hasVisibleContent: true };
|
|
953
|
+
}
|
|
954
|
+
return null;
|
|
900
955
|
}
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
956
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
957
|
+
const cellItems = (cell.items || []).filter((uid) => {
|
|
958
|
+
if (uid === EMPTY_COLUMN_UID) {
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
return modelByUid.get(uid)?.hidden !== true;
|
|
962
|
+
});
|
|
963
|
+
const hasVisibleContent = cellItems.some((uid) => {
|
|
964
|
+
if (uid === EMPTY_COLUMN_UID) return false;
|
|
965
|
+
const model = modelByUid.get(uid);
|
|
966
|
+
return !model || !model.hidden;
|
|
967
|
+
});
|
|
968
|
+
const hasEmptyPlaceholder = cellItems.includes(EMPTY_COLUMN_UID);
|
|
969
|
+
if (hasVisibleContent || hasEmptyPlaceholder) {
|
|
970
|
+
return { cell: { ...cell, items: cellItems }, size: keepSize, hasVisibleContent };
|
|
907
971
|
}
|
|
908
|
-
return
|
|
909
|
-
})
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (hasVisibleItem) {
|
|
915
|
-
cells.push({ ...cell, items: cellItems });
|
|
916
|
-
keptSizes.push(keepSize);
|
|
917
|
-
}
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
return cells.length
|
|
972
|
+
return null;
|
|
973
|
+
})
|
|
974
|
+
.filter(Boolean) as VisibleCellEntry[];
|
|
975
|
+
const hasVisibleContent = cellsWithSizes.some((entry) => entry.hasVisibleContent);
|
|
976
|
+
|
|
977
|
+
return hasVisibleContent
|
|
921
978
|
? {
|
|
922
979
|
...row,
|
|
923
|
-
cells,
|
|
924
|
-
sizes:
|
|
980
|
+
cells: cellsWithSizes.map((entry) => entry.cell),
|
|
981
|
+
sizes: cellsWithSizes.map((entry) => entry.size),
|
|
925
982
|
}
|
|
926
983
|
: null;
|
|
927
984
|
})
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { EMPTY_COLUMN_UID, FlowEngine } from '@nocobase/flow-engine';
|
|
11
11
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
12
|
-
import { GridModel } from '../GridModel';
|
|
12
|
+
import { GRID_FLOW_KEY, GRID_STEP, GridModel } from '../GridModel';
|
|
13
13
|
|
|
14
14
|
describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
|
|
15
15
|
let engine: FlowEngine;
|
|
@@ -228,8 +228,8 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
|
|
|
228
228
|
expect(sizes.row1).toEqual([10, 14]);
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
it('
|
|
232
|
-
|
|
231
|
+
it('preserves EMPTY_COLUMN placeholder width in runtime mode', () => {
|
|
232
|
+
engine.flowSettings.disable();
|
|
233
233
|
|
|
234
234
|
const visible = engine.createModel({ use: 'FlowModel', uid: 'v' });
|
|
235
235
|
|
|
@@ -237,11 +237,18 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
|
|
|
237
237
|
use: 'GridModel',
|
|
238
238
|
uid: 'grid-8',
|
|
239
239
|
props: {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
layout: {
|
|
241
|
+
version: 2,
|
|
242
|
+
rows: [
|
|
243
|
+
{
|
|
244
|
+
id: 'row1',
|
|
245
|
+
cells: [
|
|
246
|
+
{ id: 'cell1', items: ['v'] },
|
|
247
|
+
{ id: 'cell2', items: [EMPTY_COLUMN_UID] },
|
|
248
|
+
],
|
|
249
|
+
sizes: [8, 16],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
245
252
|
},
|
|
246
253
|
},
|
|
247
254
|
structure: {} as any,
|
|
@@ -250,8 +257,73 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
|
|
|
250
257
|
(model as any).subModels = { items: [visible] };
|
|
251
258
|
|
|
252
259
|
const { rows, sizes } = (model as any).getVisibleLayout();
|
|
253
|
-
//
|
|
254
|
-
expect(rows.row1).toEqual([['v']]);
|
|
255
|
-
expect(sizes.row1).toEqual([
|
|
260
|
+
// 空列是拖拽缩窄区块后的布局占位;运行态也要保留其宽度,避免剩余区块被拉满整行。
|
|
261
|
+
expect(rows.row1).toEqual([['v'], [EMPTY_COLUMN_UID]]);
|
|
262
|
+
expect(sizes.row1).toEqual([8, 16]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('removes rows that only contain EMPTY_COLUMN placeholders in runtime mode when there are no items', () => {
|
|
266
|
+
engine.flowSettings.disable();
|
|
267
|
+
|
|
268
|
+
const model = engine.createModel<GridModel>({
|
|
269
|
+
use: 'GridModel',
|
|
270
|
+
uid: 'grid-9',
|
|
271
|
+
props: {
|
|
272
|
+
layout: {
|
|
273
|
+
version: 2,
|
|
274
|
+
rows: [
|
|
275
|
+
{
|
|
276
|
+
id: 'row1',
|
|
277
|
+
cells: [{ id: 'cell1', items: [EMPTY_COLUMN_UID] }],
|
|
278
|
+
sizes: [24],
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
structure: {} as any,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
(model as any).subModels = { items: [] };
|
|
287
|
+
|
|
288
|
+
const { rows, sizes } = (model as any).getVisibleLayout();
|
|
289
|
+
expect(rows).toEqual({});
|
|
290
|
+
expect(sizes).toEqual({});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('removes the placeholder-only row after the last real item in the row is deleted', () => {
|
|
294
|
+
engine.flowSettings.disable();
|
|
295
|
+
|
|
296
|
+
const visible = engine.createModel({ use: 'FlowModel', uid: 'v' });
|
|
297
|
+
const layout = {
|
|
298
|
+
version: 2 as const,
|
|
299
|
+
rows: [
|
|
300
|
+
{
|
|
301
|
+
id: 'row1',
|
|
302
|
+
cells: [
|
|
303
|
+
{ id: 'cell1', items: ['v'] },
|
|
304
|
+
{ id: 'cell2', items: [EMPTY_COLUMN_UID] },
|
|
305
|
+
],
|
|
306
|
+
sizes: [8, 16],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
const model = engine.createModel<GridModel>({
|
|
311
|
+
use: 'GridModel',
|
|
312
|
+
uid: 'grid-10',
|
|
313
|
+
props: { layout },
|
|
314
|
+
structure: {} as any,
|
|
315
|
+
});
|
|
316
|
+
(model as any).subModels = { items: [visible] };
|
|
317
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, { layout });
|
|
318
|
+
model.syncLayoutProps(model.getGridLayout());
|
|
319
|
+
model.onMount();
|
|
320
|
+
|
|
321
|
+
(model as any).subModels = { items: [] };
|
|
322
|
+
model.emitter.emit('onSubModelDestroyed', visible);
|
|
323
|
+
|
|
324
|
+
expect(model.props.rows).toEqual({});
|
|
325
|
+
expect(model.props.sizes).toEqual({});
|
|
326
|
+
expect(model.props.layout.rows).toEqual([]);
|
|
327
|
+
expect(model.getStepParams(GRID_FLOW_KEY, GRID_STEP).layout.rows).toEqual([]);
|
|
256
328
|
});
|
|
257
329
|
});
|