@nocobase/client-v2 2.1.0-alpha.39 → 2.1.0-alpha.40
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 -1
- 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/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 +102 -90
- package/es/nocobase-buildin-plugin/index.d.ts +25 -0
- package/es/utils/appVersionHTML.d.ts +10 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/remotePlugins.d.ts +4 -1
- package/lib/index.js +108 -96
- package/package.json +7 -7
- package/src/BaseApplication.tsx +3 -3
- 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 +31 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
- package/src/__tests__/remotePlugins.test.ts +55 -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/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/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/nocobase-buildin-plugin/index.tsx +70 -16
- package/src/utils/appVersionHTML.ts +28 -0
- package/src/utils/globalDeps.ts +2 -2
- package/src/utils/index.tsx +2 -0
- package/src/utils/remotePlugins.ts +12 -7
|
@@ -0,0 +1,211 @@
|
|
|
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 { Input, type InputProps } from 'antd';
|
|
11
|
+
import type { PasswordProps as AntdPasswordProps } from 'antd/es/input';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
|
|
14
|
+
// --- Strength scoring -------------------------------------------------------
|
|
15
|
+
// Pure scoring function ported from the v1 client's password utils. Returns a
|
|
16
|
+
// bucketed score in `[20, 40, 60, 80, 100]` based on character-class diversity,
|
|
17
|
+
// length, repeated / sequential / consecutive character penalties, and a
|
|
18
|
+
// "middle non-letter / non-symbol" bonus. No external dependencies — safe to
|
|
19
|
+
// run on any string.
|
|
20
|
+
//
|
|
21
|
+
// Kept private to this module on purpose. Callers consume the visual strength
|
|
22
|
+
// bar via `<PasswordInput checkStrength>`; they shouldn't need to compute the
|
|
23
|
+
// raw score themselves.
|
|
24
|
+
|
|
25
|
+
const isNum = (c: number) => c >= 48 && c <= 57;
|
|
26
|
+
const isLower = (c: number) => c >= 97 && c <= 122;
|
|
27
|
+
const isUpper = (c: number) => c >= 65 && c <= 90;
|
|
28
|
+
const isSymbol = (c: number) => !(isLower(c) || isUpper(c) || isNum(c));
|
|
29
|
+
const isLetter = (c: number) => isLower(c) || isUpper(c);
|
|
30
|
+
|
|
31
|
+
function getStrength(val: string): number {
|
|
32
|
+
if (!val) return 0;
|
|
33
|
+
let num = 0;
|
|
34
|
+
let lower = 0;
|
|
35
|
+
let upper = 0;
|
|
36
|
+
let symbol = 0;
|
|
37
|
+
let MNS = 0;
|
|
38
|
+
let rep = 0;
|
|
39
|
+
let repC = 0;
|
|
40
|
+
let consecutive = 0;
|
|
41
|
+
let sequential = 0;
|
|
42
|
+
const len = () => num + lower + upper + symbol;
|
|
43
|
+
const callMe = () => {
|
|
44
|
+
let re = num > 0 ? 1 : 0;
|
|
45
|
+
re += lower > 0 ? 1 : 0;
|
|
46
|
+
re += upper > 0 ? 1 : 0;
|
|
47
|
+
re += symbol > 0 ? 1 : 0;
|
|
48
|
+
return re > 2 && len() >= 8 ? re + 1 : 0;
|
|
49
|
+
};
|
|
50
|
+
for (let i = 0; i < val.length; i++) {
|
|
51
|
+
const c = val.charCodeAt(i);
|
|
52
|
+
if (isNum(c)) {
|
|
53
|
+
num++;
|
|
54
|
+
if (i !== 0 && i !== val.length - 1) MNS++;
|
|
55
|
+
if (i > 0 && isNum(val.charCodeAt(i - 1))) consecutive++;
|
|
56
|
+
} else if (isLower(c)) {
|
|
57
|
+
lower++;
|
|
58
|
+
if (i > 0 && isLower(val.charCodeAt(i - 1))) consecutive++;
|
|
59
|
+
} else if (isUpper(c)) {
|
|
60
|
+
upper++;
|
|
61
|
+
if (i > 0 && isUpper(val.charCodeAt(i - 1))) consecutive++;
|
|
62
|
+
} else {
|
|
63
|
+
symbol++;
|
|
64
|
+
if (i !== 0 && i !== val.length - 1) MNS++;
|
|
65
|
+
}
|
|
66
|
+
let exists = false;
|
|
67
|
+
for (let j = 0; j < val.length; j++) {
|
|
68
|
+
if (val[i] === val[j] && i !== j) {
|
|
69
|
+
exists = true;
|
|
70
|
+
repC += Math.abs(val.length / (j - i));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (exists) {
|
|
74
|
+
rep++;
|
|
75
|
+
const unique = val.length - rep;
|
|
76
|
+
repC = unique ? Math.ceil(repC / unique) : Math.ceil(repC);
|
|
77
|
+
}
|
|
78
|
+
if (i > 1) {
|
|
79
|
+
const last1 = val.charCodeAt(i - 1);
|
|
80
|
+
const last2 = val.charCodeAt(i - 2);
|
|
81
|
+
if (isLetter(c)) {
|
|
82
|
+
if (isLetter(last1) && isLetter(last2)) {
|
|
83
|
+
const v = val.toLowerCase();
|
|
84
|
+
const vi = v.charCodeAt(i);
|
|
85
|
+
const vi1 = v.charCodeAt(i - 1);
|
|
86
|
+
const vi2 = v.charCodeAt(i - 2);
|
|
87
|
+
if (vi - vi1 === vi1 - vi2 && Math.abs(vi - vi1) === 1) sequential++;
|
|
88
|
+
}
|
|
89
|
+
} else if (isNum(c)) {
|
|
90
|
+
if (isNum(last1) && isNum(last2)) {
|
|
91
|
+
if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) sequential++;
|
|
92
|
+
}
|
|
93
|
+
} else if (isSymbol(last1) && isSymbol(last2)) {
|
|
94
|
+
if (c - last1 === last1 - last2 && Math.abs(c - last1) === 1) sequential++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let sum = 0;
|
|
99
|
+
const length = len();
|
|
100
|
+
sum += 4 * length;
|
|
101
|
+
if (lower > 0) sum += 2 * (length - lower);
|
|
102
|
+
if (upper > 0) sum += 2 * (length - upper);
|
|
103
|
+
if (num !== length) sum += 4 * num;
|
|
104
|
+
sum += 6 * symbol;
|
|
105
|
+
sum += 2 * MNS;
|
|
106
|
+
sum += 2 * callMe();
|
|
107
|
+
if (length === lower + upper) sum -= length;
|
|
108
|
+
if (length === num) sum -= num;
|
|
109
|
+
sum -= repC;
|
|
110
|
+
sum -= 2 * consecutive;
|
|
111
|
+
sum -= 3 * sequential;
|
|
112
|
+
sum = Math.max(0, Math.min(100, sum));
|
|
113
|
+
|
|
114
|
+
if (sum >= 80) return 100;
|
|
115
|
+
if (sum >= 60) return 80;
|
|
116
|
+
if (sum >= 40) return 60;
|
|
117
|
+
if (sum >= 20) return 40;
|
|
118
|
+
return 20;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Strength bar UI --------------------------------------------------------
|
|
122
|
+
// Colours and pixel sizes are intentionally kept identical to v1's
|
|
123
|
+
// `PasswordStrength` so the visual remains unchanged across the v1 → v2
|
|
124
|
+
// migration. When the design system formalises tokens for "strength signal"
|
|
125
|
+
// colours, swap these literals for the matching token expressions.
|
|
126
|
+
|
|
127
|
+
const segmentDividerStyle: React.CSSProperties = {
|
|
128
|
+
position: 'absolute',
|
|
129
|
+
zIndex: 1,
|
|
130
|
+
height: 8,
|
|
131
|
+
top: 0,
|
|
132
|
+
background: '#fff',
|
|
133
|
+
width: 1,
|
|
134
|
+
transform: 'translate(-50%, 0)',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
function StrengthBar({ score }: { score: number }) {
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
style={{
|
|
141
|
+
background: '#e0e0e0',
|
|
142
|
+
marginBottom: 3,
|
|
143
|
+
position: 'relative',
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{/* Four white dividers split the bar into five strength brackets that
|
|
147
|
+
line up with the bucketed scoring in `getStrength`. */}
|
|
148
|
+
<div style={{ ...segmentDividerStyle, left: '20%' }} />
|
|
149
|
+
<div style={{ ...segmentDividerStyle, left: '40%' }} />
|
|
150
|
+
<div style={{ ...segmentDividerStyle, left: '60%' }} />
|
|
151
|
+
<div style={{ ...segmentDividerStyle, left: '80%' }} />
|
|
152
|
+
{/* The full gradient is always laid down, then `clip-path` trims it back
|
|
153
|
+
to the current score percentage — gives a smooth fill animation on
|
|
154
|
+
value change without re-painting the gradient on every render. */}
|
|
155
|
+
<div
|
|
156
|
+
style={{
|
|
157
|
+
position: 'relative',
|
|
158
|
+
backgroundImage: '-webkit-linear-gradient(left, #ff5500, #ff9300)',
|
|
159
|
+
transition: 'all 0.35s ease-in-out',
|
|
160
|
+
height: 8,
|
|
161
|
+
width: '100%',
|
|
162
|
+
marginTop: 5,
|
|
163
|
+
clipPath: `polygon(0 0,${score}% 0,${score}% 100%,0 100%)`,
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- Public component -------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
export interface PasswordInputProps extends AntdPasswordProps {
|
|
173
|
+
/**
|
|
174
|
+
* Render a visual strength bar beneath the input. Defaults to `false`. The
|
|
175
|
+
* score is computed locally — opting in does NOT add any form validation;
|
|
176
|
+
* use a separate `Form.Item.rules` entry for that (or wire the entry up to
|
|
177
|
+
* a cross-plugin password-validator extension point if your project
|
|
178
|
+
* provides one).
|
|
179
|
+
*/
|
|
180
|
+
checkStrength?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `Input.Password` plus an optional strength meter, ported from the v1
|
|
185
|
+
* `Password` component. The strength scoring and bar UI are identical to v1,
|
|
186
|
+
* so users who switch from a v1 page to a v2 page see the same visual signal.
|
|
187
|
+
*
|
|
188
|
+
* The component is value-shape compatible with antd `Input.Password` — drop
|
|
189
|
+
* it into any existing `Form.Item<password>` and toggle the meter with
|
|
190
|
+
* `checkStrength`.
|
|
191
|
+
*
|
|
192
|
+
* Caveats:
|
|
193
|
+
*
|
|
194
|
+
* - Strength scoring is purely a UX hint, not validation. Submitting a weak
|
|
195
|
+
* password is still allowed unless the server (or a separately installed
|
|
196
|
+
* password-policy plugin) rejects it.
|
|
197
|
+
* - The meter swallows the gap between `<Input.Password>` and the next form
|
|
198
|
+
* element. If your `Form.Item` already adds vertical rhythm, the meter
|
|
199
|
+
* inherits it; no extra spacing is added.
|
|
200
|
+
*/
|
|
201
|
+
export function PasswordInput(props: PasswordInputProps) {
|
|
202
|
+
const { value, checkStrength, ...rest } = props;
|
|
203
|
+
return (
|
|
204
|
+
<span>
|
|
205
|
+
<Input.Password {...(rest as InputProps)} value={value} />
|
|
206
|
+
{checkStrength ? <StrengthBar score={getStrength(String(value || ''))} /> : null}
|
|
207
|
+
</span>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default PasswordInput;
|
|
@@ -0,0 +1,137 @@
|
|
|
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 { useRequest } from 'ahooks';
|
|
11
|
+
import { Select, type SelectProps } from 'antd';
|
|
12
|
+
import React, { useMemo } from 'react';
|
|
13
|
+
|
|
14
|
+
export interface RemoteSelectFieldNames {
|
|
15
|
+
label?: string;
|
|
16
|
+
value?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RemoteSelectProps<RawItem = any, Resp = RawItem[], V = any>
|
|
20
|
+
extends Omit<SelectProps<V>, 'options' | 'loading'> {
|
|
21
|
+
/**
|
|
22
|
+
* Fetch the option source. Receives no arguments; caller closes over the
|
|
23
|
+
* `ctx.api.resource(...)` (or any other source) it needs. May resolve
|
|
24
|
+
* with either an array of raw items (the common case) or an arbitrary
|
|
25
|
+
* envelope object — in the latter case, supply `selectItems` to pluck
|
|
26
|
+
* the array out.
|
|
27
|
+
*/
|
|
28
|
+
request: () => Promise<Resp | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* When `request` returns an envelope (object with metadata around the
|
|
31
|
+
* list), use this to extract the array of items that drives the
|
|
32
|
+
* dropdown. Defaults to identity, i.e. `request` itself returns the
|
|
33
|
+
* array.
|
|
34
|
+
*/
|
|
35
|
+
selectItems?: (response: Resp) => RawItem[] | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Names of the raw item properties that hold the display label and the
|
|
38
|
+
* persisted value. Defaults to `{ label: 'label', value: 'value' }`.
|
|
39
|
+
* Ignored when `mapOptions` is supplied.
|
|
40
|
+
*/
|
|
41
|
+
fieldNames?: RemoteSelectFieldNames;
|
|
42
|
+
/**
|
|
43
|
+
* Full custom mapping from a raw item to an antd `OptionType`. When
|
|
44
|
+
* provided, overrides `fieldNames`.
|
|
45
|
+
*/
|
|
46
|
+
mapOptions?: (item: RawItem, index: number) => { label: React.ReactNode; value: any };
|
|
47
|
+
/**
|
|
48
|
+
* Stable cache key for ahooks `useRequest` so the dropdown doesn't re-fetch
|
|
49
|
+
* on every re-mount. Pass a value tied to the request's effective inputs.
|
|
50
|
+
*/
|
|
51
|
+
cacheKey?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Re-run the request when any of these values changes. Forwarded to
|
|
54
|
+
* `useRequest`'s `refreshDeps`.
|
|
55
|
+
*/
|
|
56
|
+
refreshDeps?: unknown[];
|
|
57
|
+
/**
|
|
58
|
+
* Skip the auto-fetch on mount when `false`. Defaults to `true`.
|
|
59
|
+
*/
|
|
60
|
+
ready?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Notified once the request resolves. Receives both the mapped item
|
|
63
|
+
* array and the raw response envelope — useful when callers need to
|
|
64
|
+
* read sibling metadata (counts, availability hints, etc.) without
|
|
65
|
+
* issuing a second request.
|
|
66
|
+
*/
|
|
67
|
+
onLoaded?: (items: RawItem[], response: Resp) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generic settings-page Select bound to an async option source. The
|
|
72
|
+
* component itself stays framework-agnostic — it knows nothing about
|
|
73
|
+
* NocoBase resources, data sources, or Formily. Pass any async `request`
|
|
74
|
+
* that resolves with an array, and supply `fieldNames` (or `mapOptions`)
|
|
75
|
+
* to map raw items to antd option shape.
|
|
76
|
+
*
|
|
77
|
+
* Search is local-only (antd's default `optionFilterProp="label"`). For
|
|
78
|
+
* server-side search, drive `request` from external state and pass the
|
|
79
|
+
* search input via `refreshDeps`.
|
|
80
|
+
*/
|
|
81
|
+
export function RemoteSelect<RawItem = any, Resp = RawItem[], V = any>(props: RemoteSelectProps<RawItem, Resp, V>) {
|
|
82
|
+
const {
|
|
83
|
+
request,
|
|
84
|
+
selectItems,
|
|
85
|
+
fieldNames,
|
|
86
|
+
mapOptions,
|
|
87
|
+
cacheKey,
|
|
88
|
+
refreshDeps,
|
|
89
|
+
ready = true,
|
|
90
|
+
onLoaded,
|
|
91
|
+
showSearch = true,
|
|
92
|
+
allowClear = true,
|
|
93
|
+
...selectProps
|
|
94
|
+
} = props;
|
|
95
|
+
|
|
96
|
+
const { data: response, loading } = useRequest<Resp | undefined, []>(request, {
|
|
97
|
+
cacheKey,
|
|
98
|
+
refreshDeps,
|
|
99
|
+
ready,
|
|
100
|
+
onSuccess: (resp) => {
|
|
101
|
+
if (resp === undefined) return;
|
|
102
|
+
const items = (selectItems ? selectItems(resp) : (resp as unknown as RawItem[])) || [];
|
|
103
|
+
onLoaded?.(items, resp);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const items = useMemo<RawItem[]>(() => {
|
|
108
|
+
if (response === undefined) return [];
|
|
109
|
+
return (selectItems ? selectItems(response) : (response as unknown as RawItem[])) || [];
|
|
110
|
+
}, [response, selectItems]);
|
|
111
|
+
|
|
112
|
+
const labelKey = fieldNames?.label ?? 'label';
|
|
113
|
+
const valueKey = fieldNames?.value ?? 'value';
|
|
114
|
+
|
|
115
|
+
const options = useMemo(() => {
|
|
116
|
+
if (mapOptions) {
|
|
117
|
+
return items.map((item, index) => mapOptions(item, index));
|
|
118
|
+
}
|
|
119
|
+
return items.map((item: any) => ({
|
|
120
|
+
label: item?.[labelKey],
|
|
121
|
+
value: item?.[valueKey],
|
|
122
|
+
}));
|
|
123
|
+
}, [items, mapOptions, labelKey, valueKey]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Select
|
|
127
|
+
{...selectProps}
|
|
128
|
+
showSearch={showSearch}
|
|
129
|
+
allowClear={allowClear}
|
|
130
|
+
optionFilterProp="label"
|
|
131
|
+
loading={loading}
|
|
132
|
+
options={options}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default RemoteSelect;
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export * from './createFormRegistry';
|
|
11
|
+
export * from './DialogFormLayout';
|
|
11
12
|
export * from './DrawerFormLayout';
|
|
12
13
|
export * from './EnvVariableInput';
|
|
13
14
|
export * from './FileSizeInput';
|
|
14
15
|
export * from './JsonTextArea';
|
|
16
|
+
export * from './PasswordInput';
|
|
17
|
+
export * from './RemoteSelect';
|
|
15
18
|
export * from './VariableInput';
|
|
@@ -19,7 +19,7 @@ import React, { useMemo, useState } from 'react';
|
|
|
19
19
|
import { SortableRow, SortHandle } from './dnd/SortableRow';
|
|
20
20
|
import { RowOverlayPreview } from './RowOverlayPreview';
|
|
21
21
|
import { SelectionCell } from './SelectionCell';
|
|
22
|
-
import { indexSwapClassName, selectionGutterClassName } from './styles';
|
|
22
|
+
import { indexSwapClassName, selectionGutterClassName, tableScrollClassName } from './styles';
|
|
23
23
|
import { readRowKey, snapshotSourceRow, type RowKey, type RowSnapshot } from './utils';
|
|
24
24
|
|
|
25
25
|
type RowSelectionRenderCellResult<RecordType> = React.ReactNode | RenderedCell<RecordType>;
|
|
@@ -227,6 +227,7 @@ export function Table<RecordType extends object = any>(props: TableProps<RecordT
|
|
|
227
227
|
|
|
228
228
|
const tableClassName = cx(
|
|
229
229
|
className,
|
|
230
|
+
tableScrollClassName,
|
|
230
231
|
showHandleInSelection && selectionGutterClassName,
|
|
231
232
|
showIndex && rowSelection && indexSwapClassName,
|
|
232
233
|
);
|
|
@@ -10,6 +10,25 @@
|
|
|
10
10
|
import { css } from '@emotion/css';
|
|
11
11
|
import { SORT_HANDLE_GUTTER } from './constants';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Restore horizontal scrolling on `.ant-table-content` so wide tables in
|
|
15
|
+
* narrow containers (drawer / settings panel) scroll their inner `<table>`
|
|
16
|
+
* instead of getting clipped or forcing the outer container to grow.
|
|
17
|
+
*
|
|
18
|
+
* `width: max-content` on the inner `<table>` lets columns size to their
|
|
19
|
+
* natural width; `min-width: 100%` keeps the table filling the viewport
|
|
20
|
+
* when total column width is smaller than the container.
|
|
21
|
+
*/
|
|
22
|
+
export const tableScrollClassName = css`
|
|
23
|
+
&.ant-table-wrapper .ant-table-content {
|
|
24
|
+
overflow: auto hidden;
|
|
25
|
+
}
|
|
26
|
+
&.ant-table-wrapper .ant-table-content > table {
|
|
27
|
+
width: max-content;
|
|
28
|
+
min-width: 100%;
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
|
|
13
32
|
/**
|
|
14
33
|
* Reserve a `SORT_HANDLE_GUTTER`-wide gap on the left of the rowSelection
|
|
15
34
|
* column so the handle's `left:0` lands inside a `position:relative` cell.
|
package/src/components/index.ts
CHANGED
|
@@ -12,5 +12,7 @@ export * from './BlankComponent';
|
|
|
12
12
|
export * from './form/table/dnd';
|
|
13
13
|
export * from './form';
|
|
14
14
|
export * from './Icon';
|
|
15
|
+
export * from './PoweredBy';
|
|
15
16
|
export * from './RouterContextCleaner';
|
|
17
|
+
export * from './SwitchLanguage';
|
|
16
18
|
export * from './form/table';
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { TinyColor } from '@ctrl/tinycolor';
|
|
11
11
|
import { useEffect } from 'react';
|
|
12
12
|
import { theme } from 'antd';
|
|
13
|
-
import {
|
|
13
|
+
import { defaultTheme } from '../theme';
|
|
14
|
+
import type { CustomToken } from '../theme';
|
|
14
15
|
|
|
15
16
|
interface Result extends ReturnType<typeof theme.useToken> {
|
|
16
17
|
token: CustomToken;
|
|
@@ -42,6 +43,10 @@ export const CSSVariableProvider = ({ children }) => {
|
|
|
42
43
|
document.body.style.setProperty('--colorWarningBg', token.colorWarningBg);
|
|
43
44
|
document.body.style.setProperty('--colorWarningBorder', token.colorWarningBorder);
|
|
44
45
|
document.body.style.setProperty('--colorText', token.colorText);
|
|
46
|
+
document.body.style.setProperty('--colorTextDescription', token.colorTextDescription);
|
|
47
|
+
document.body.style.setProperty('--colorBgTextHover', token.colorBgTextHover);
|
|
48
|
+
document.body.style.setProperty('--colorSplit', token.colorSplit);
|
|
49
|
+
document.body.style.setProperty('--borderRadiusOuter', `${token.borderRadiusOuter}px`);
|
|
45
50
|
document.body.style.setProperty('--colorTextHeaderMenu', token.colorTextHeaderMenu);
|
|
46
51
|
document.body.style.setProperty('--colorPrimaryText', token.colorPrimaryText);
|
|
47
52
|
document.body.style.setProperty('--colorPrimaryTextActive', token.colorPrimaryTextActive);
|
|
@@ -81,9 +86,13 @@ export const CSSVariableProvider = ({ children }) => {
|
|
|
81
86
|
token.colorPrimaryTextActive,
|
|
82
87
|
token.colorPrimaryTextHover,
|
|
83
88
|
token.colorSettings,
|
|
89
|
+
token.colorBgTextHover,
|
|
90
|
+
token.colorSplit,
|
|
84
91
|
token.colorText,
|
|
92
|
+
token.colorTextDescription,
|
|
85
93
|
token.colorWarningBg,
|
|
86
94
|
token.colorWarningBorder,
|
|
95
|
+
token.borderRadiusOuter,
|
|
87
96
|
token.controlHeightLG,
|
|
88
97
|
token.marginLG,
|
|
89
98
|
token.marginSM,
|
|
@@ -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 {
|
|
@@ -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]);
|