@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,314 @@
|
|
|
1
|
+
# client-v2 components
|
|
2
|
+
|
|
3
|
+
This folder collects the React components that `@nocobase/client-v2` exposes to downstream plugins. Components are organized by directory — at the moment the main one is `form/`, which targets settings pages and form-shaped UIs.
|
|
4
|
+
|
|
5
|
+
Skim this before writing a new plugin so you don't reinvent the wheel. Components are mostly orthogonal — import only what you need.
|
|
6
|
+
|
|
7
|
+
## form/
|
|
8
|
+
|
|
9
|
+
Components under `form/` cover the "settings page + form" shape. The typical recipe: open a form container with `ctx.viewer.drawer` / `ctx.viewer.dialog`, host an antd `Form` + `Form.Item` tree inside, and pick standard field controls from this folder.
|
|
10
|
+
|
|
11
|
+
Grouped by purpose: form containers, form fields, data table, utilities.
|
|
12
|
+
|
|
13
|
+
### Form containers
|
|
14
|
+
|
|
15
|
+
#### DrawerFormLayout
|
|
16
|
+
|
|
17
|
+
Drawer-style form layout. Pair with `ctx.viewer.drawer({ content })`.
|
|
18
|
+
|
|
19
|
+
- Top: a close icon next to the title. Clicking close fires `onCancel` and dismisses the drawer
|
|
20
|
+
- Bottom: default Cancel / Submit buttons; override the whole footer with `footer`
|
|
21
|
+
- Middle: caller-supplied `<Form>` instance + fields
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { DrawerFormLayout } from '@nocobase/client-v2';
|
|
25
|
+
|
|
26
|
+
ctx.viewer.drawer({
|
|
27
|
+
width: '50%',
|
|
28
|
+
content: () => (
|
|
29
|
+
<DrawerFormLayout
|
|
30
|
+
title={t('Add authenticator')}
|
|
31
|
+
onSubmit={handleSubmit}
|
|
32
|
+
submitting={submitting}
|
|
33
|
+
>
|
|
34
|
+
<Form form={form} layout="vertical">
|
|
35
|
+
{/* fields */}
|
|
36
|
+
</Form>
|
|
37
|
+
</DrawerFormLayout>
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Key props:
|
|
43
|
+
|
|
44
|
+
- `title`: title node (rendered next to the close icon)
|
|
45
|
+
- `onCancel` / `onSubmit`: callbacks; the drawer closes automatically once they resolve. Throw from `onSubmit` to keep the drawer open (e.g. on a validation error)
|
|
46
|
+
- `submitting`: drives the Submit button's loading state
|
|
47
|
+
- `submitText` / `cancelText`: button labels
|
|
48
|
+
- `footer`: full override of the footer content (replaces the default Cancel + Submit pair)
|
|
49
|
+
|
|
50
|
+
#### DialogFormLayout
|
|
51
|
+
|
|
52
|
+
Dialog-style form layout, the centered counterpart of `DrawerFormLayout`. Pair with `ctx.viewer.dialog({ closable: true, content })`.
|
|
53
|
+
|
|
54
|
+
The only visual difference from the drawer version: the title is a bare string (no inline close icon), relying on antd Modal's native top-right X. Note that `viewer.dialog` disables antd's native X by default — you have to pass `closable: true` explicitly for it to appear.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { DialogFormLayout } from '@nocobase/client-v2';
|
|
58
|
+
|
|
59
|
+
ctx.viewer.dialog({
|
|
60
|
+
closable: true, // restore antd Modal's native top-right X
|
|
61
|
+
content: () => (
|
|
62
|
+
<DialogFormLayout title={t('Bind verifier')} onSubmit={handleSubmit}>
|
|
63
|
+
<Form form={form} layout="vertical">
|
|
64
|
+
{/* fields */}
|
|
65
|
+
</Form>
|
|
66
|
+
</DialogFormLayout>
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
When to pick which:
|
|
72
|
+
|
|
73
|
+
- **Drawer**: long forms with lots of fields that benefit from a full-height side panel (settings-page "Add / Edit")
|
|
74
|
+
- **Dialog**: short forms that ask for quick confirmation (bind, change password, two-factor verify)
|
|
75
|
+
|
|
76
|
+
Props are identical to `DrawerFormLayout` — they're drop-in replacements at the API level.
|
|
77
|
+
|
|
78
|
+
### Form fields
|
|
79
|
+
|
|
80
|
+
#### RemoteSelect
|
|
81
|
+
|
|
82
|
+
A Select bound to an async option source. Framework-level — it knows nothing about NocoBase business resources; the caller passes a `request` function that fetches whatever it needs.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { RemoteSelect } from '@nocobase/client-v2';
|
|
86
|
+
|
|
87
|
+
<Form.Item name="provider" label={t('Provider')}>
|
|
88
|
+
<RemoteSelect<{ name: string; title: string }>
|
|
89
|
+
request={async () => {
|
|
90
|
+
const response = await ctx.api.resource('smsOTPProviders').list();
|
|
91
|
+
return response?.data?.data || [];
|
|
92
|
+
}}
|
|
93
|
+
cacheKey="@nocobase/plugin-verification:smsOTPProviders:list"
|
|
94
|
+
mapOptions={(item) => ({ label: compileT(item.title), value: item.name })}
|
|
95
|
+
/>
|
|
96
|
+
</Form.Item>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Key props:
|
|
100
|
+
|
|
101
|
+
- `request: () => Promise`: fetch function, required. Returns either an array of items or an envelope object (combine with `selectItems` to pluck the array out)
|
|
102
|
+
- `selectItems`: extractor that takes the `request` result and returns the option array. Use when the response is `{ items, meta }`-shaped
|
|
103
|
+
- `fieldNames`: defaults to `{ label, value }` mapping; override with `mapOptions` when the raw item doesn't match
|
|
104
|
+
- `mapOptions: (item, index) => ({ label, value })`: full override of option mapping
|
|
105
|
+
- `cacheKey` / `refreshDeps` / `ready`: forwarded to ahooks `useRequest`; control caching and refresh timing
|
|
106
|
+
- `onLoaded: (items, response) => void`: fires after data arrives; receives both the mapped item array and the raw response
|
|
107
|
+
|
|
108
|
+
All other antd `Select` props (`mode` / `placeholder` / `disabled` / `value` / `onChange` / etc.) are passed through.
|
|
109
|
+
|
|
110
|
+
`showSearch` + `allowClear` are on by default; search is local (filters by label). For server-side search, drive the search input through external state and pass it via `refreshDeps`, then read it inside `request`.
|
|
111
|
+
|
|
112
|
+
#### EnvVariableInput
|
|
113
|
+
|
|
114
|
+
A variable input restricted to the `$env` namespace. Designed for secret / credential fields — supports environment-variable references and adds password masking for plain literal values.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { EnvVariableInput } from '@nocobase/client-v2';
|
|
118
|
+
|
|
119
|
+
<Form.Item name={['options', 'accessKeySecret']} label={t('Access Key Secret')}>
|
|
120
|
+
<EnvVariableInput password />
|
|
121
|
+
</Form.Item>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Key props:
|
|
125
|
+
|
|
126
|
+
- `password`: when enabled, non-variable literal values render through `Input.Password` so they're masked. Variable expressions like `{{ $env.X }}` stay visible and editable
|
|
127
|
+
- `placeholder` / `disabled` / `value` / `onChange`: standard controlled-input props
|
|
128
|
+
|
|
129
|
+
The persisted value is always a string: either a literal (`'literal'`) or a server-template reference (`'{{ $env.foo.bar }}'`). The server expands the reference at use time.
|
|
130
|
+
|
|
131
|
+
#### VariableInput / VariableTextArea
|
|
132
|
+
|
|
133
|
+
General-purpose variable inputs. Can reference any namespace registered on `flowEngine.context` — `$env`, `$user`, plus ad-hoc business namespaces like `$resetLink`.
|
|
134
|
+
|
|
135
|
+
The two differ in shape:
|
|
136
|
+
|
|
137
|
+
- `VariableInput`: single-line. Variables render as colored pills (compact "chips")
|
|
138
|
+
- `VariableTextArea`: multi-line. Variables stay as raw `{{ ... }}` text — better for email templates and other long-form content where the literal `{{ ... }}` is the intended display (the server expands them at render time)
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
|
|
142
|
+
|
|
143
|
+
// Email subject — single line, pills
|
|
144
|
+
<Form.Item name={['options', 'emailSubject']} label={t('Subject')}>
|
|
145
|
+
<VariableInput
|
|
146
|
+
namespaces={['$env']}
|
|
147
|
+
extraNodes={[
|
|
148
|
+
{ name: '$resetLink', title: t('Reset password link'), type: 'string', paths: ['$resetLink'] },
|
|
149
|
+
]}
|
|
150
|
+
/>
|
|
151
|
+
</Form.Item>
|
|
152
|
+
|
|
153
|
+
// Email body — multi-line, literal
|
|
154
|
+
<Form.Item name={['options', 'emailContentHTML']} label={t('Content')}>
|
|
155
|
+
<VariableTextArea namespaces={['$env']} rows={10} />
|
|
156
|
+
</Form.Item>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Key props:
|
|
160
|
+
|
|
161
|
+
- `namespaces`: restrict the picker to specific top-level namespaces. Omit to expose every registered top-level property
|
|
162
|
+
- `extraNodes`: static leaves appended after the namespace-filtered nodes. Use for variables that only make sense in the current page (e.g. `$resetLink`)
|
|
163
|
+
- `converters`: override the default path ↔ string converters. `EnvVariableInput` uses this hook to lock its output to `$env`
|
|
164
|
+
- `value` / `onChange` / `placeholder` / `disabled`: standard controlled-input props
|
|
165
|
+
|
|
166
|
+
Under the hood `VariableInput` wraps `VariableHybridInput` (inline pills), `VariableTextArea` wraps `TextAreaWithContextSelector` (textarea + variable button). Both share the same MetaTree.
|
|
167
|
+
|
|
168
|
+
#### FileSizeInput
|
|
169
|
+
|
|
170
|
+
A byte-valued size input paired with a unit selector (Byte / KB / MB / GB). The persisted value is always in bytes; the displayed number is derived from the picked unit.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { FileSizeInput } from '@nocobase/client-v2';
|
|
174
|
+
|
|
175
|
+
<Form.Item name="maxFileSize" label={t('Max file size')}>
|
|
176
|
+
<FileSizeInput min={1} max={1024 * 1024 * 1024} defaultValue={20 * 1024 * 1024} />
|
|
177
|
+
</Form.Item>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Key props:
|
|
181
|
+
|
|
182
|
+
- `min` / `max`: allowed byte range; values out of range snap back on blur. Defaults: `min=1`, `max=Infinity`
|
|
183
|
+
- `defaultValue`: drives the initial unit when the field is empty (e.g. 20 MB starts in the "MB" unit)
|
|
184
|
+
- `value` / `onChange`: controlled-input contract; the value type is `number` (bytes)
|
|
185
|
+
|
|
186
|
+
#### PasswordInput
|
|
187
|
+
|
|
188
|
+
`antd Input.Password` plus an optional strength meter, ported from v1's
|
|
189
|
+
`Password` component. Use for any "set / change password" form when you want
|
|
190
|
+
to give the user the same visual signal they had in v1.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { PasswordInput } from '@nocobase/client-v2';
|
|
194
|
+
|
|
195
|
+
<Form.Item name="newPassword" label={t('New password')} rules={[{ required: true }]}>
|
|
196
|
+
<PasswordInput autoComplete="new-password" checkStrength />
|
|
197
|
+
</Form.Item>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Key props:
|
|
201
|
+
|
|
202
|
+
- `checkStrength`: render a strength bar beneath the input. Defaults to `false`. The score is bucketed `[20, 40, 60, 80, 100]` and shown via a clipped gradient (orange) inside a grey track, matching v1
|
|
203
|
+
- All other antd `Input.Password` props are passed through unchanged: `value` / `onChange` / `disabled` / `placeholder` / `autoComplete` / etc.
|
|
204
|
+
|
|
205
|
+
The strength meter is purely a UX hint, NOT validation. Submitting a weak password is still allowed unless the server (or a separately installed password-policy plugin) rejects it. Wire up real password rules through `Form.Item.rules` or — when the open-source ↔ commercial extension point lands — the project's shared password-validator hook.
|
|
206
|
+
|
|
207
|
+
#### JsonTextArea
|
|
208
|
+
|
|
209
|
+
JSON input. The stored value is a JS object (not a string) — parsing happens live while typing and is finalized on blur.
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import { JsonTextArea } from '@nocobase/client-v2';
|
|
213
|
+
|
|
214
|
+
<Form.Item name="customConfig" label={t('Custom config')}>
|
|
215
|
+
<JsonTextArea rows={6} json5 />
|
|
216
|
+
</Form.Item>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Key props:
|
|
220
|
+
|
|
221
|
+
- `space`: serialization indent. Defaults to `2`
|
|
222
|
+
- `json5`: parse with JSON5 (tolerates trailing commas, comments, single quotes, etc.). Defaults to `false`
|
|
223
|
+
- `showError`: render the parse error inline below the textarea. Defaults to `true`
|
|
224
|
+
- All other antd `Input.TextArea` props are passed through
|
|
225
|
+
|
|
226
|
+
`value` / `onChange` are typed as `unknown` because JSON values can be any shape. Tighten the contract with validators in `Form.Item.rules`.
|
|
227
|
+
|
|
228
|
+
### Data table
|
|
229
|
+
|
|
230
|
+
#### Table
|
|
231
|
+
|
|
232
|
+
The standard settings-page table, built on antd `Table` with two additions:
|
|
233
|
+
|
|
234
|
+
1. **Row index ↔ checkbox swap**: by default each row shows its ordinal ("1 / 2 / 3"); on hover or when selected the cell flips to a checkbox. The two elements are absolute-positioned in the same cell so neither steals layout space. Requires `rowSelection` to be present
|
|
235
|
+
2. **Drag-and-drop reorder**: pass `isDraggable` to enable. Each row gets a drag handle on the left; `onSortEnd` fires when a row is dropped. The component does NOT mutate `dataSource` — the caller persists the move (`resource.move(...)`) and `refresh()`s
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';
|
|
239
|
+
|
|
240
|
+
<Table<AuthenticatorRecord>
|
|
241
|
+
rowKey="id"
|
|
242
|
+
loading={loading}
|
|
243
|
+
columns={columns}
|
|
244
|
+
dataSource={data?.records || []}
|
|
245
|
+
isDraggable
|
|
246
|
+
onSortEnd={async (from, to) => {
|
|
247
|
+
await resource.move({ sourceId: from.id, targetId: to.id });
|
|
248
|
+
refresh();
|
|
249
|
+
}}
|
|
250
|
+
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
|
251
|
+
pagination={{
|
|
252
|
+
current: page,
|
|
253
|
+
pageSize,
|
|
254
|
+
total: data?.total || 0,
|
|
255
|
+
onChange: (next, nextSize) => { /* ... */ },
|
|
256
|
+
}}
|
|
257
|
+
/>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Key props:
|
|
261
|
+
|
|
262
|
+
- `rowKey`: required. Drag-sort and row-identity both depend on it
|
|
263
|
+
- `showIndex`: defaults to `true`; disable to keep the cell at checkbox-only
|
|
264
|
+
- `isDraggable`: drag-and-drop toggle. Defaults to `false` — when off the component is a thin antd `Table` superset
|
|
265
|
+
- `onSortEnd: (from, to) => void | Promise`: fired when a row is dropped. Caller persists
|
|
266
|
+
- `showSortHandle`: defaults to `true`; set false when you want the handle off (or embedded into a custom column via `<SortHandle />`)
|
|
267
|
+
- All other antd `Table` props are passed through
|
|
268
|
+
|
|
269
|
+
Companion exports:
|
|
270
|
+
|
|
271
|
+
- `DEFAULT_PAGE_SIZE` (value `50`): suggested default page size
|
|
272
|
+
- `PAGE_SIZE_OPTIONS`: suggested page-size dropdown values `[5, 10, 20, 50, 100, 200]`
|
|
273
|
+
- `SortHandle`: standalone handle component, exported from `@nocobase/client-v2` for embedding into custom columns
|
|
274
|
+
|
|
275
|
+
### Utilities
|
|
276
|
+
|
|
277
|
+
#### createFormRegistry
|
|
278
|
+
|
|
279
|
+
Factory for a namespaced "entry registry". Each call returns an independent registry instance backed by its own closure `Map`.
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { createFormRegistry, type FormRegistryEntry } from '@nocobase/client-v2';
|
|
283
|
+
|
|
284
|
+
interface StorageType extends FormRegistryEntry {
|
|
285
|
+
// FormRegistryEntry requires at least `name: string`
|
|
286
|
+
title: string;
|
|
287
|
+
Component: React.ComponentType;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const storageTypes = createFormRegistry<StorageType>('file-manager/storage-types');
|
|
291
|
+
|
|
292
|
+
storageTypes.register({ name: 'local', title: 'Local storage', Component: LocalStorageForm });
|
|
293
|
+
storageTypes.register({ name: 's3', title: 'Amazon S3', Component: S3StorageForm });
|
|
294
|
+
|
|
295
|
+
storageTypes.get('s3');
|
|
296
|
+
storageTypes.list();
|
|
297
|
+
storageTypes.has('local');
|
|
298
|
+
storageTypes.unregister('local');
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Use this when a plugin needs an extension point for "same name + same shape + different implementation" things (the file-manager's storage types, the verification plugin's OTP providers, etc.). It's a thin wrapper around `Map` that adds a namespace label and an HMR-friendly overwrite warning.
|
|
302
|
+
|
|
303
|
+
Re-registering the same `name` overwrites the previous entry and emits a `console.warn` — HMR doesn't throw, and unintended duplicates surface in dev.
|
|
304
|
+
|
|
305
|
+
## When to add a new component here
|
|
306
|
+
|
|
307
|
+
- Two or more plugins need the same field or container shape — promote it to this folder
|
|
308
|
+
- Cross-plugin reusable, but the abstraction couples to a specific business domain (e.g. "pick a verifier", "pick a data source") — keep it inside the producing plugin and export from that plugin's `client-v2/`
|
|
309
|
+
- Before reaching for abstraction, check whether an existing component can be improved instead. `RemoteSelect.selectItems` is an example — it landed so envelope responses don't need their own component
|
|
310
|
+
|
|
311
|
+
Two follow-ups after adding a new component:
|
|
312
|
+
|
|
313
|
+
1. Add `export * from './XxxComponent'` to `form/index.tsx`
|
|
314
|
+
2. Document it here so the next plugin migration finds it
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# client-v2 components
|
|
2
|
+
|
|
3
|
+
这里收纳 `@nocobase/client-v2` 暴露给业务插件复用的一组 React 组件。组件按目录组织——目前主要是 `form/`,给设置页和表单场景用。
|
|
4
|
+
|
|
5
|
+
写新插件前先翻一遍这份说明,能省下不少重复造轮子的功夫。组件之间互相耦合很少,按需 import 就行。
|
|
6
|
+
|
|
7
|
+
## form/
|
|
8
|
+
|
|
9
|
+
`form/` 目录下的组件围绕「设置页 + 表单」这一类场景。常见用法是配合 `ctx.viewer.drawer` / `ctx.viewer.dialog` 打开一个表单容器,里面放 antd 的 `Form` + `Form.Item`,字段用这里提供的标准控件。
|
|
10
|
+
|
|
11
|
+
下面按用途分四组:表单容器、表单字段、数据表、工具。
|
|
12
|
+
|
|
13
|
+
### 表单容器
|
|
14
|
+
|
|
15
|
+
#### DrawerFormLayout
|
|
16
|
+
|
|
17
|
+
抽屉形态的表单 layout。配合 `ctx.viewer.drawer({ content })` 用。
|
|
18
|
+
|
|
19
|
+
- 顶部 Header:左上角一个 close 图标 + 标题。点击 close 会触发 `onCancel` 然后关闭抽屉
|
|
20
|
+
- 底部 Footer:默认 Cancel / Submit 两个按钮;可以用 `footer` 完全替换
|
|
21
|
+
- 中间 children:调用方自己放 `<Form>` 实例 + 字段
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { DrawerFormLayout } from '@nocobase/client-v2';
|
|
25
|
+
|
|
26
|
+
ctx.viewer.drawer({
|
|
27
|
+
width: '50%',
|
|
28
|
+
content: () => (
|
|
29
|
+
<DrawerFormLayout
|
|
30
|
+
title={t('添加认证器')}
|
|
31
|
+
onSubmit={handleSubmit}
|
|
32
|
+
submitting={submitting}
|
|
33
|
+
>
|
|
34
|
+
<Form form={form} layout="vertical">
|
|
35
|
+
{/* 字段 */}
|
|
36
|
+
</Form>
|
|
37
|
+
</DrawerFormLayout>
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
主要属性:
|
|
43
|
+
|
|
44
|
+
- `title`:标题节点(旁边带 close 图标)
|
|
45
|
+
- `onCancel` / `onSubmit`:回调,resolve 后会自动关闭抽屉。Submit 里 throw 可以让抽屉保持打开(比如校验失败)
|
|
46
|
+
- `submitting`:驱动 Submit 按钮的 loading
|
|
47
|
+
- `submitText` / `cancelText`:按钮文字
|
|
48
|
+
- `footer`:完全自定义 Footer 内容(覆盖默认两个按钮)
|
|
49
|
+
|
|
50
|
+
#### DialogFormLayout
|
|
51
|
+
|
|
52
|
+
弹窗形态的表单 layout,跟 `DrawerFormLayout` 是同源对偶。配合 `ctx.viewer.dialog({ closable: true, content })` 用。
|
|
53
|
+
|
|
54
|
+
跟 Drawer 版本的差异只有一点:title 是裸字符串(不带 close 图标),依赖 antd Modal 自带的右上角 X。注意 `viewer.dialog` 默认会禁用 antd 的原生 X——必须显式传 `closable: true` 才会出现。
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { DialogFormLayout } from '@nocobase/client-v2';
|
|
58
|
+
|
|
59
|
+
ctx.viewer.dialog({
|
|
60
|
+
closable: true, // 关键:开启 antd Modal 原生右上角 X
|
|
61
|
+
content: () => (
|
|
62
|
+
<DialogFormLayout title={t('绑定验证码')} onSubmit={handleSubmit}>
|
|
63
|
+
<Form form={form} layout="vertical">
|
|
64
|
+
{/* 字段 */}
|
|
65
|
+
</Form>
|
|
66
|
+
</DialogFormLayout>
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
什么时候选哪个?
|
|
72
|
+
|
|
73
|
+
- **Drawer**:长表单、字段多、需要从一侧滑出占用整面(比如设置页的「添加 / 编辑」)
|
|
74
|
+
- **Dialog**:短表单、需要快速确认(比如绑定、修改密码、二次验证)
|
|
75
|
+
|
|
76
|
+
属性跟 `DrawerFormLayout` 完全一致,可以直接换。
|
|
77
|
+
|
|
78
|
+
### 表单字段
|
|
79
|
+
|
|
80
|
+
#### RemoteSelect
|
|
81
|
+
|
|
82
|
+
异步拉数据的 Select。框架级组件——不感知 NocoBase 业务,调用方传一个 `request` 函数自己拉数据。
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { RemoteSelect } from '@nocobase/client-v2';
|
|
86
|
+
|
|
87
|
+
<Form.Item name="provider" label={t('服务商')}>
|
|
88
|
+
<RemoteSelect<{ name: string; title: string }>
|
|
89
|
+
request={async () => {
|
|
90
|
+
const response = await ctx.api.resource('smsOTPProviders').list();
|
|
91
|
+
return response?.data?.data || [];
|
|
92
|
+
}}
|
|
93
|
+
cacheKey="@nocobase/plugin-verification:smsOTPProviders:list"
|
|
94
|
+
mapOptions={(item) => ({ label: compileT(item.title), value: item.name })}
|
|
95
|
+
/>
|
|
96
|
+
</Form.Item>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
主要属性:
|
|
100
|
+
|
|
101
|
+
- `request: () => Promise`:拉数据,必填。可以返回数组,也可以返回带元数据的对象(搭配 `selectItems` 取出数组)
|
|
102
|
+
- `selectItems`:从 `request` 返回值中抽出数组的函数。响应体是 `{ items, meta }` 形态时用
|
|
103
|
+
- `fieldNames`:默认按 `{ label, value }` 字段映射;不匹配的时候用 `mapOptions` 完全自定义
|
|
104
|
+
- `mapOptions: (item, index) => ({ label, value })`:完整覆盖映射逻辑
|
|
105
|
+
- `cacheKey` / `refreshDeps` / `ready`:透传给 ahooks `useRequest`,控制缓存和 refresh 时机
|
|
106
|
+
- `onLoaded: (items, response) => void`:拉到数据后的回调,能拿到原始响应
|
|
107
|
+
|
|
108
|
+
剩下的 antd `Select` props(`mode` / `placeholder` / `disabled` / `value` / `onChange` 等)都原样透传。
|
|
109
|
+
|
|
110
|
+
默认开启 `showSearch` + `allowClear`,搜索是本地模式(按 label 匹配)。要做服务端搜索,把搜索词放进 `refreshDeps`,在 `request` 里读出来发请求。
|
|
111
|
+
|
|
112
|
+
#### EnvVariableInput
|
|
113
|
+
|
|
114
|
+
`$env` 命名空间的变量输入器。专门给「密钥 / 凭证」这类字段用——支持环境变量引用,同时给纯字面值加 password mask。
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { EnvVariableInput } from '@nocobase/client-v2';
|
|
118
|
+
|
|
119
|
+
<Form.Item name={['options', 'accessKeySecret']} label={t('Access Key Secret')}>
|
|
120
|
+
<EnvVariableInput password />
|
|
121
|
+
</Form.Item>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
主要属性:
|
|
125
|
+
|
|
126
|
+
- `password`:开启后,非变量的字面值会用 `Input.Password` 形态遮盖。变量表达式(比如 `{{ $env.X }}`)依然可见可编辑
|
|
127
|
+
- `placeholder` / `disabled` / `value` / `onChange`:标准受控字段属性
|
|
128
|
+
|
|
129
|
+
值的形态是字符串:`'literal'` 或 `'{{ $env.foo.bar }}'`。服务端在使用时再展开成实际值。
|
|
130
|
+
|
|
131
|
+
#### VariableInput / VariableTextArea
|
|
132
|
+
|
|
133
|
+
通用变量输入器。可以引用任意 `flowEngine.context` 注册的命名空间(`$env` / `$user` / 业务自定义的 `$resetLink` 等)。
|
|
134
|
+
|
|
135
|
+
两者差异:
|
|
136
|
+
|
|
137
|
+
- `VariableInput`:单行,变量渲染成彩色 pill(视觉上是「短标签」)
|
|
138
|
+
- `VariableTextArea`:多行,变量保留 `{{ ... }}` 字面——适合邮件模板这种「字面 + 变量」混排的长文本
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
|
|
142
|
+
|
|
143
|
+
// 邮件主题:单行 pill 形态
|
|
144
|
+
<Form.Item name={['options', 'emailSubject']} label={t('主题')}>
|
|
145
|
+
<VariableInput
|
|
146
|
+
namespaces={['$env']}
|
|
147
|
+
extraNodes={[
|
|
148
|
+
{ name: '$resetLink', title: t('重置密码链接'), type: 'string', paths: ['$resetLink'] },
|
|
149
|
+
]}
|
|
150
|
+
/>
|
|
151
|
+
</Form.Item>
|
|
152
|
+
|
|
153
|
+
// 邮件正文:多行字面
|
|
154
|
+
<Form.Item name={['options', 'emailContentHTML']} label={t('正文')}>
|
|
155
|
+
<VariableTextArea namespaces={['$env']} rows={10} />
|
|
156
|
+
</Form.Item>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
主要属性:
|
|
160
|
+
|
|
161
|
+
- `namespaces`:限定可选的顶层命名空间。不传就用 `flowEngine.context` 里全部已注册的
|
|
162
|
+
- `extraNodes`:在命名空间过滤后追加几条静态变量(用于 `$resetLink` 这类只在当前页面有意义的局部变量)
|
|
163
|
+
- `converters`:覆盖默认的 path ↔ string 转换器。`EnvVariableInput` 就是用这个钩子把输出锁定到 `$env`
|
|
164
|
+
- `value` / `onChange` / `placeholder` / `disabled`:标准受控字段属性
|
|
165
|
+
|
|
166
|
+
底层共用 `VariableHybridInput`(`VariableInput`)和 `TextAreaWithContextSelector`(`VariableTextArea`),用同一套 MetaTree 数据。
|
|
167
|
+
|
|
168
|
+
#### FileSizeInput
|
|
169
|
+
|
|
170
|
+
文件大小输入器。值统一存字节数,UI 上配一个单位选择器(Byte / KB / MB / GB)。
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { FileSizeInput } from '@nocobase/client-v2';
|
|
174
|
+
|
|
175
|
+
<Form.Item name="maxFileSize" label={t('单文件大小上限')}>
|
|
176
|
+
<FileSizeInput min={1} max={1024 * 1024 * 1024} defaultValue={20 * 1024 * 1024} />
|
|
177
|
+
</Form.Item>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
主要属性:
|
|
181
|
+
|
|
182
|
+
- `min` / `max`:允许的字节数区间,blur 时会自动 clamp 回界内。默认 `min=1`、`max=Infinity`
|
|
183
|
+
- `defaultValue`:用来决定初次显示的单位(比如默认 20 MB 就会以 MB 单位起始)
|
|
184
|
+
- `value` / `onChange`:受控字段,值类型是 `number`(字节)
|
|
185
|
+
|
|
186
|
+
#### PasswordInput
|
|
187
|
+
|
|
188
|
+
antd `Input.Password` 加一个可选的强度提示条,从 v1 的 `Password` 组件移植过来。用于「设置 / 修改密码」类表单——v1 → v2 迁移过来后视觉信号保持一致。
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { PasswordInput } from '@nocobase/client-v2';
|
|
192
|
+
|
|
193
|
+
<Form.Item name="newPassword" label={t('新密码')} rules={[{ required: true }]}>
|
|
194
|
+
<PasswordInput autoComplete="new-password" checkStrength />
|
|
195
|
+
</Form.Item>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
主要属性:
|
|
199
|
+
|
|
200
|
+
- `checkStrength`:在输入框下方渲染一条强度提示。默认 `false`。强度评分按 `[20, 40, 60, 80, 100]` 分桶,用裁剪的橙色渐变填充在灰色底条上,配色跟 v1 保持一致
|
|
201
|
+
- 其他 antd `Input.Password` 属性原样透传:`value` / `onChange` / `disabled` / `placeholder` / `autoComplete` 等
|
|
202
|
+
|
|
203
|
+
强度条只是 UX 提示,**不是表单校验**。弱密码仍然能提交,除非 server(或单独安装的 password-policy 商业插件)拒绝。真正的密码规则通过 `Form.Item.rules` 或——等开源 ↔ 商业的 extension point 落地之后——项目共享的 password validator hook 接入。
|
|
204
|
+
|
|
205
|
+
#### JsonTextArea
|
|
206
|
+
|
|
207
|
+
JSON 输入器。存的值是 JS 对象(不是字符串),编辑时实时解析、blur 时校验。
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
import { JsonTextArea } from '@nocobase/client-v2';
|
|
211
|
+
|
|
212
|
+
<Form.Item name="customConfig" label={t('自定义配置')}>
|
|
213
|
+
<JsonTextArea rows={6} json5 />
|
|
214
|
+
</Form.Item>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
主要属性:
|
|
218
|
+
|
|
219
|
+
- `space`:序列化缩进,默认 `2`
|
|
220
|
+
- `json5`:开启后用 JSON5 解析(容忍尾逗号、注释、单引号等)。默认关
|
|
221
|
+
- `showError`:解析失败时是否在下方显示错误消息。默认 `true`
|
|
222
|
+
- 其他 antd `Input.TextArea` 的属性都透传
|
|
223
|
+
|
|
224
|
+
`value` / `onChange` 的类型是 `unknown`——因为 JSON 可以是任意结构。调用方按业务约束在 `Form.Item.rules` 里加 validator 收紧类型。
|
|
225
|
+
|
|
226
|
+
### 数据表
|
|
227
|
+
|
|
228
|
+
#### Table
|
|
229
|
+
|
|
230
|
+
设置页表格的标准组件,基于 antd `Table` 扩展了两点:
|
|
231
|
+
|
|
232
|
+
1. **行索引和复选框切换**:默认状态显示「1 / 2 / 3」行号,悬停或选中时切换成 checkbox。两个元素绝对定位在同一格内,不会抢空间。需要 `rowSelection` 才生效
|
|
233
|
+
2. **拖拽排序**:传 `isDraggable` 开启,每行左侧出现拖拽手柄;放下时触发 `onSortEnd`。组件不动 `dataSource`,由调用方在回调里跑 `resource.move(...)` 再 `refresh()`
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';
|
|
237
|
+
|
|
238
|
+
<Table<AuthenticatorRecord>
|
|
239
|
+
rowKey="id"
|
|
240
|
+
loading={loading}
|
|
241
|
+
columns={columns}
|
|
242
|
+
dataSource={data?.records || []}
|
|
243
|
+
isDraggable
|
|
244
|
+
onSortEnd={async (from, to) => {
|
|
245
|
+
await resource.move({ sourceId: from.id, targetId: to.id });
|
|
246
|
+
refresh();
|
|
247
|
+
}}
|
|
248
|
+
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
|
249
|
+
pagination={{
|
|
250
|
+
current: page,
|
|
251
|
+
pageSize,
|
|
252
|
+
total: data?.total || 0,
|
|
253
|
+
onChange: (next, nextSize) => { /* ... */ },
|
|
254
|
+
}}
|
|
255
|
+
/>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
主要属性:
|
|
259
|
+
|
|
260
|
+
- `rowKey`:必填。拖拽和行身份识别都依赖它
|
|
261
|
+
- `showIndex`:默认 `true`,关掉就只显示 checkbox
|
|
262
|
+
- `isDraggable`:开关拖拽。默认 `false`,关掉就是个加强版 antd Table
|
|
263
|
+
- `onSortEnd: (from, to) => void | Promise`:拖拽放下时触发。调用方负责持久化
|
|
264
|
+
- `showSortHandle`:默认 `true`,需要时可以隐藏手柄,自己在某列里嵌 `<SortHandle />`
|
|
265
|
+
- 其他 antd `Table` props 全部透传
|
|
266
|
+
|
|
267
|
+
附带导出:
|
|
268
|
+
|
|
269
|
+
- `DEFAULT_PAGE_SIZE`(值 `50`):建议的默认分页大小
|
|
270
|
+
- `PAGE_SIZE_OPTIONS`:建议的分页选项 `[5, 10, 20, 50, 100, 200]`
|
|
271
|
+
- `SortHandle`:从 `@nocobase/client-v2` 导出的独立手柄组件,可以嵌进自定义列
|
|
272
|
+
|
|
273
|
+
### 工具
|
|
274
|
+
|
|
275
|
+
#### createFormRegistry
|
|
276
|
+
|
|
277
|
+
带命名空间的「条目注册表」工厂。每次调用返回一个独立的 registry 实例,闭包持有自己的 `Map`。
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { createFormRegistry, type FormRegistryEntry } from '@nocobase/client-v2';
|
|
281
|
+
|
|
282
|
+
interface StorageType extends FormRegistryEntry {
|
|
283
|
+
// FormRegistryEntry 要求至少有 `name: string`
|
|
284
|
+
title: string;
|
|
285
|
+
Component: React.ComponentType;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const storageTypes = createFormRegistry<StorageType>('file-manager/storage-types');
|
|
289
|
+
|
|
290
|
+
storageTypes.register({ name: 'local', title: '本地存储', Component: LocalStorageForm });
|
|
291
|
+
storageTypes.register({ name: 's3', title: 'Amazon S3', Component: S3StorageForm });
|
|
292
|
+
|
|
293
|
+
storageTypes.get('s3');
|
|
294
|
+
storageTypes.list();
|
|
295
|
+
storageTypes.has('local');
|
|
296
|
+
storageTypes.unregister('local');
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
主要用在:插件需要给「同名 + 同形 + 不同实现」的东西做扩展点(比如 file-manager 的存储类型、verification 的 OTP provider)。比 `Map` 多了 namespace 标识和 HMR 友好的覆盖警告。
|
|
300
|
+
|
|
301
|
+
`name` 重复注册会用新条目覆盖旧的,同时打 `console.warn`——HMR 时不抛错,开发期能看到意外的重复。
|
|
302
|
+
|
|
303
|
+
## 怎么决定加不加新组件
|
|
304
|
+
|
|
305
|
+
- 出现两个及以上插件需要同一形态的字段或容器——抽到这里
|
|
306
|
+
- 跨插件复用、但耦合到具体业务领域(比如「选一个 verifier」「选一个数据源」)——留在业务插件里,从插件的 `client-v2/` 自己 export
|
|
307
|
+
- 抽象前先看现有组件能不能改进:比如 `RemoteSelect` 的 `selectItems` 就是为了让带元数据的响应不需要再开新组件
|
|
308
|
+
|
|
309
|
+
新增组件后别忘记两件事:
|
|
310
|
+
|
|
311
|
+
1. 在 `form/index.tsx` 加一行 `export * from './XxxComponent'`
|
|
312
|
+
2. 回来这份 README 补一节,方便后续插件迁移时找到
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { TranslationOutlined } from '@ant-design/icons';
|
|
11
|
+
import { Dropdown, theme } from 'antd';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { useApp } from '../hooks/useApp';
|
|
14
|
+
import { useSystemSettings } from '../flow/system-settings';
|
|
15
|
+
import languageCodes from '../locale/languageCodes';
|
|
16
|
+
|
|
17
|
+
export function SwitchLanguage() {
|
|
18
|
+
const app = useApp();
|
|
19
|
+
const { token } = theme.useToken();
|
|
20
|
+
const { data } = useSystemSettings() || {};
|
|
21
|
+
const enabledLanguages: string[] = data?.data?.enabledLanguages || [];
|
|
22
|
+
|
|
23
|
+
if (enabledLanguages.length <= 1) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const items = enabledLanguages
|
|
28
|
+
.filter((code) => languageCodes[code])
|
|
29
|
+
.map((code) => ({ key: code, label: languageCodes[code].label }));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Dropdown
|
|
33
|
+
menu={{
|
|
34
|
+
selectable: true,
|
|
35
|
+
defaultSelectedKeys: [app.apiClient.auth.locale],
|
|
36
|
+
items,
|
|
37
|
+
onClick: ({ key }) => {
|
|
38
|
+
app.apiClient.auth.setLocale(String(key));
|
|
39
|
+
window.location.reload();
|
|
40
|
+
},
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<TranslationOutlined style={{ fontSize: token.fontSizeXL, color: 'inherit' }} />
|
|
44
|
+
</Dropdown>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default SwitchLanguage;
|