@nocobase/client-v2 2.1.0-beta.35 → 2.1.0-beta.37

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.
Files changed (94) hide show
  1. package/es/BaseApplication.d.ts +2 -1
  2. package/es/components/PoweredBy.d.ts +18 -0
  3. package/es/components/SwitchLanguage.d.ts +11 -0
  4. package/es/components/form/DialogFormLayout.d.ts +51 -0
  5. package/es/components/form/DrawerFormLayout.d.ts +11 -11
  6. package/es/components/form/PasswordInput.d.ts +40 -0
  7. package/es/components/form/RemoteSelect.d.ts +79 -0
  8. package/es/components/form/filter/CollectionFilter.d.ts +41 -0
  9. package/es/components/form/filter/CollectionFilterItem.d.ts +41 -0
  10. package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
  11. package/es/components/form/filter/FilterValueInput.d.ts +29 -0
  12. package/es/components/form/filter/index.d.ts +11 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
  14. package/es/components/form/index.d.ts +4 -0
  15. package/es/components/form/table/styles.d.ts +10 -0
  16. package/es/components/index.d.ts +2 -0
  17. package/es/data-source/ExtendCollectionsProvider.d.ts +24 -0
  18. package/es/data-source/index.d.ts +9 -0
  19. package/es/flow/components/filter/index.d.ts +2 -0
  20. package/es/flow/components/filter/useFilterOptions.d.ts +54 -0
  21. package/es/flow/models/base/ActionModelCore.d.ts +6 -0
  22. package/es/flow/models/base/GridModel.d.ts +2 -0
  23. package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
  24. package/es/flow-compat/passwordUtils.d.ts +1 -1
  25. package/es/hooks/index.d.ts +2 -0
  26. package/es/hooks/useCurrentAppInfo.d.ts +9 -0
  27. package/es/index.d.ts +1 -0
  28. package/es/index.mjs +109 -92
  29. package/es/nocobase-buildin-plugin/index.d.ts +20 -2
  30. package/es/utils/appVersionHTML.d.ts +10 -0
  31. package/es/utils/index.d.ts +1 -0
  32. package/es/utils/remotePlugins.d.ts +4 -1
  33. package/lib/index.js +115 -98
  34. package/package.json +7 -7
  35. package/src/BaseApplication.tsx +16 -3
  36. package/src/PluginSettingsManager.ts +2 -1
  37. package/src/__tests__/PluginSettingsManager.test.ts +19 -0
  38. package/src/__tests__/PoweredBy.test.tsx +130 -0
  39. package/src/__tests__/app.test.tsx +40 -0
  40. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
  41. package/src/__tests__/remotePlugins.test.ts +55 -0
  42. package/src/__tests__/useCurrentRoles.test.tsx +100 -0
  43. package/src/components/PoweredBy.tsx +71 -0
  44. package/src/components/README.md +397 -0
  45. package/src/components/README.zh-CN.md +394 -0
  46. package/src/components/SwitchLanguage.tsx +48 -0
  47. package/src/components/form/DialogFormLayout.tsx +87 -0
  48. package/src/components/form/DrawerFormLayout.tsx +13 -32
  49. package/src/components/form/PasswordInput.tsx +211 -0
  50. package/src/components/form/RemoteSelect.tsx +137 -0
  51. package/src/components/form/filter/CollectionFilter.tsx +101 -0
  52. package/src/components/form/filter/CollectionFilterItem.tsx +176 -0
  53. package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
  54. package/src/components/form/filter/FilterValueInput.tsx +198 -0
  55. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +205 -0
  56. package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
  57. package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
  58. package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
  59. package/src/components/form/filter/index.ts +13 -0
  60. package/src/components/form/filter/useFilterActionProps.ts +200 -0
  61. package/src/components/form/index.tsx +4 -0
  62. package/src/components/form/table/Table.tsx +2 -1
  63. package/src/components/form/table/styles.ts +19 -0
  64. package/src/components/index.ts +2 -0
  65. package/src/css-variable/CSSVariableProvider.tsx +10 -1
  66. package/src/data-source/ExtendCollectionsProvider.tsx +70 -0
  67. package/src/data-source/index.ts +10 -0
  68. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
  69. package/src/flow/actions/dataScope.tsx +3 -0
  70. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
  71. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/filter/index.ts +3 -0
  74. package/src/flow/components/filter/useFilterOptions.ts +80 -0
  75. package/src/flow/models/base/ActionModel.tsx +8 -7
  76. package/src/flow/models/base/ActionModelCore.tsx +15 -7
  77. package/src/flow/models/base/GridModel.tsx +93 -36
  78. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
  79. package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
  80. package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
  81. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
  82. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
  83. package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
  84. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
  85. package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
  86. package/src/hooks/index.ts +2 -0
  87. package/src/hooks/useCurrentAppInfo.ts +36 -0
  88. package/src/index.ts +1 -0
  89. package/src/nocobase-buildin-plugin/index.tsx +66 -18
  90. package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
  91. package/src/utils/appVersionHTML.ts +28 -0
  92. package/src/utils/globalDeps.ts +2 -2
  93. package/src/utils/index.tsx +2 -0
  94. package/src/utils/remotePlugins.ts +12 -7
@@ -0,0 +1,71 @@
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 { css, cx } from '@emotion/css';
11
+ import { parseHTML } from '@nocobase/utils/client';
12
+ import { theme } from 'antd';
13
+ import React from 'react';
14
+ import { useTranslation } from 'react-i18next';
15
+ import { useCurrentAppInfo } from '../hooks/useCurrentAppInfo';
16
+ import { usePlugin } from '../hooks/usePlugin';
17
+ import { getAppVersionHTML } from '../utils/appVersionHTML';
18
+
19
+ const homePageUrls: Record<string, string> = {
20
+ 'en-US': 'https://www.nocobase.com',
21
+ 'zh-CN': 'https://www.nocobase.com/cn/',
22
+ };
23
+
24
+ /**
25
+ * Footer brand rendered on auth pages and other layout entry points. Falls
26
+ * back to "Powered by NocoBase" when `@nocobase/plugin-custom-brand` is not
27
+ * installed; otherwise renders the plugin's HTML template with the
28
+ * `{{appVersion}}` placeholder substituted. The version is escaped via
29
+ * `getAppVersionHTML` so a malicious app version cannot inject script tags.
30
+ */
31
+ export function PoweredBy() {
32
+ const { i18n } = useTranslation();
33
+ const { token } = theme.useToken();
34
+ const customBrandPlugin: any = usePlugin('@nocobase/plugin-custom-brand');
35
+ const appInfo = useCurrentAppInfo();
36
+ const appVersion = getAppVersionHTML(appInfo?.version);
37
+ const homePage = homePageUrls[i18n.language] || homePageUrls['en-US'];
38
+ const brandStyle = css`
39
+ text-align: center;
40
+ color: ${token.colorTextDescription};
41
+ a {
42
+ color: ${token.colorTextDescription};
43
+ &:hover {
44
+ color: ${token.colorText};
45
+ }
46
+ }
47
+ `;
48
+ const customBrand = customBrandPlugin?.options?.options?.brand;
49
+
50
+ if (customBrand) {
51
+ return (
52
+ <div
53
+ className={cx(brandStyle, 'nb-brand')}
54
+ dangerouslySetInnerHTML={{
55
+ __html: parseHTML(customBrand, { appVersion }),
56
+ }}
57
+ />
58
+ );
59
+ }
60
+
61
+ return (
62
+ <div className={brandStyle}>
63
+ Powered by{' '}
64
+ <a href={homePage} target="_blank" rel="noreferrer">
65
+ NocoBase
66
+ </a>
67
+ </div>
68
+ );
69
+ }
70
+
71
+ export default PoweredBy;
@@ -0,0 +1,397 @@
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({ closable: true, content })`.
18
+
19
+ - Top: title only; the native close X is rendered by antd Drawer — you must pass `closable: true` on the `viewer.drawer` call for it to appear
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
+ closable: true, // restore antd Drawer's native close X
29
+ content: () => (
30
+ <DrawerFormLayout
31
+ title={t('Add authenticator')}
32
+ onSubmit={handleSubmit}
33
+ submitting={submitting}
34
+ >
35
+ <Form form={form} layout="vertical">
36
+ {/* fields */}
37
+ </Form>
38
+ </DrawerFormLayout>
39
+ ),
40
+ });
41
+ ```
42
+
43
+ Key props:
44
+
45
+ - `title`: title node
46
+ - `onSubmit`: callback; the drawer closes automatically once it resolves. Throw to keep the drawer open (e.g. on a validation error)
47
+ - `submitting`: drives the Submit button's loading state
48
+ - `submitText` / `cancelText`: button labels
49
+ - `footer`: full override of the footer content (replaces the default Cancel + Submit pair)
50
+
51
+ To intercept close (e.g. dirty-form confirmation), use the lower-level `viewer.drawer({ preventClose, beforeClose })` hooks — this layout no longer wraps a custom cancel handler.
52
+
53
+ #### DialogFormLayout
54
+
55
+ Dialog-style form layout, the centered counterpart of `DrawerFormLayout`. Pair with `ctx.viewer.dialog({ closable: true, content })`.
56
+
57
+ The only visual difference from the drawer version is where the native close X sits — antd Drawer renders it at the top-left of the title bar, antd Modal at the top-right. Both layouts rely on the caller passing `closable: true` at the viewer call site; neither renders a close icon itself.
58
+
59
+ ```tsx
60
+ import { DialogFormLayout } from '@nocobase/client-v2';
61
+
62
+ ctx.viewer.dialog({
63
+ closable: true, // restore antd Modal's native top-right X
64
+ content: () => (
65
+ <DialogFormLayout title={t('Bind verifier')} onSubmit={handleSubmit}>
66
+ <Form form={form} layout="vertical">
67
+ {/* fields */}
68
+ </Form>
69
+ </DialogFormLayout>
70
+ ),
71
+ });
72
+ ```
73
+
74
+ When to pick which:
75
+
76
+ - **Drawer**: long forms with lots of fields that benefit from a full-height side panel (settings-page "Add / Edit")
77
+ - **Dialog**: short forms that ask for quick confirmation (bind, change password, two-factor verify)
78
+
79
+ Props are nearly identical to `DrawerFormLayout`, with one extra: `DialogFormLayout` accepts an `onCancel` callback (fired by both the Cancel button and the native X) for "discard changes" confirmations.
80
+
81
+ ### Form fields
82
+
83
+ #### RemoteSelect
84
+
85
+ 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.
86
+
87
+ ```tsx
88
+ import { RemoteSelect } from '@nocobase/client-v2';
89
+
90
+ <Form.Item name="provider" label={t('Provider')}>
91
+ <RemoteSelect<{ name: string; title: string }>
92
+ request={async () => {
93
+ const response = await ctx.api.resource('smsOTPProviders').list();
94
+ return response?.data?.data || [];
95
+ }}
96
+ cacheKey="@nocobase/plugin-verification:smsOTPProviders:list"
97
+ mapOptions={(item) => ({ label: compileT(item.title), value: item.name })}
98
+ />
99
+ </Form.Item>
100
+ ```
101
+
102
+ Key props:
103
+
104
+ - `request: () => Promise`: fetch function, required. Returns either an array of items or an envelope object (combine with `selectItems` to pluck the array out)
105
+ - `selectItems`: extractor that takes the `request` result and returns the option array. Use when the response is `{ items, meta }`-shaped
106
+ - `fieldNames`: defaults to `{ label, value }` mapping; override with `mapOptions` when the raw item doesn't match
107
+ - `mapOptions: (item, index) => ({ label, value })`: full override of option mapping
108
+ - `cacheKey` / `refreshDeps` / `ready`: forwarded to ahooks `useRequest`; control caching and refresh timing
109
+ - `onLoaded: (items, response) => void`: fires after data arrives; receives both the mapped item array and the raw response
110
+
111
+ All other antd `Select` props (`mode` / `placeholder` / `disabled` / `value` / `onChange` / etc.) are passed through.
112
+
113
+ `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`.
114
+
115
+ #### EnvVariableInput
116
+
117
+ 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.
118
+
119
+ ```tsx
120
+ import { EnvVariableInput } from '@nocobase/client-v2';
121
+
122
+ <Form.Item name={['options', 'accessKeySecret']} label={t('Access Key Secret')}>
123
+ <EnvVariableInput password />
124
+ </Form.Item>
125
+ ```
126
+
127
+ Key props:
128
+
129
+ - `password`: when enabled, non-variable literal values render through `Input.Password` so they're masked. Variable expressions like `{{ $env.X }}` stay visible and editable
130
+ - `placeholder` / `disabled` / `value` / `onChange`: standard controlled-input props
131
+
132
+ 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.
133
+
134
+ #### VariableInput / VariableTextArea
135
+
136
+ General-purpose variable inputs. Can reference any namespace registered on `flowEngine.context` — `$env`, `$user`, plus ad-hoc business namespaces like `$resetLink`.
137
+
138
+ The two differ in shape:
139
+
140
+ - `VariableInput`: single-line. Variables render as colored pills (compact "chips")
141
+ - `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)
142
+
143
+ ```tsx
144
+ import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
145
+
146
+ // Email subject — single line, pills
147
+ <Form.Item name={['options', 'emailSubject']} label={t('Subject')}>
148
+ <VariableInput
149
+ namespaces={['$env']}
150
+ extraNodes={[
151
+ { name: '$resetLink', title: t('Reset password link'), type: 'string', paths: ['$resetLink'] },
152
+ ]}
153
+ />
154
+ </Form.Item>
155
+
156
+ // Email body — multi-line, literal
157
+ <Form.Item name={['options', 'emailContentHTML']} label={t('Content')}>
158
+ <VariableTextArea namespaces={['$env']} rows={10} />
159
+ </Form.Item>
160
+ ```
161
+
162
+ Key props:
163
+
164
+ - `namespaces`: restrict the picker to specific top-level namespaces. Omit to expose every registered top-level property
165
+ - `extraNodes`: static leaves appended after the namespace-filtered nodes. Use for variables that only make sense in the current page (e.g. `$resetLink`)
166
+ - `converters`: override the default path ↔ string converters. `EnvVariableInput` uses this hook to lock its output to `$env`
167
+ - `value` / `onChange` / `placeholder` / `disabled`: standard controlled-input props
168
+
169
+ Under the hood `VariableInput` wraps `VariableHybridInput` (inline pills), `VariableTextArea` wraps `TextAreaWithContextSelector` (textarea + variable button). Both share the same MetaTree.
170
+
171
+ #### FileSizeInput
172
+
173
+ 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.
174
+
175
+ ```tsx
176
+ import { FileSizeInput } from '@nocobase/client-v2';
177
+
178
+ <Form.Item name="maxFileSize" label={t('Max file size')}>
179
+ <FileSizeInput min={1} max={1024 * 1024 * 1024} defaultValue={20 * 1024 * 1024} />
180
+ </Form.Item>
181
+ ```
182
+
183
+ Key props:
184
+
185
+ - `min` / `max`: allowed byte range; values out of range snap back on blur. Defaults: `min=1`, `max=Infinity`
186
+ - `defaultValue`: drives the initial unit when the field is empty (e.g. 20 MB starts in the "MB" unit)
187
+ - `value` / `onChange`: controlled-input contract; the value type is `number` (bytes)
188
+
189
+ #### PasswordInput
190
+
191
+ `antd Input.Password` plus an optional strength meter, ported from v1's
192
+ `Password` component. Use for any "set / change password" form when you want
193
+ to give the user the same visual signal they had in v1.
194
+
195
+ ```tsx
196
+ import { PasswordInput } from '@nocobase/client-v2';
197
+
198
+ <Form.Item name="newPassword" label={t('New password')} rules={[{ required: true }]}>
199
+ <PasswordInput autoComplete="new-password" checkStrength />
200
+ </Form.Item>
201
+ ```
202
+
203
+ Key props:
204
+
205
+ - `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
206
+ - All other antd `Input.Password` props are passed through unchanged: `value` / `onChange` / `disabled` / `placeholder` / `autoComplete` / etc.
207
+
208
+ 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.
209
+
210
+ #### JsonTextArea
211
+
212
+ JSON input. The stored value is a JS object (not a string) — parsing happens live while typing and is finalized on blur.
213
+
214
+ ```tsx
215
+ import { JsonTextArea } from '@nocobase/client-v2';
216
+
217
+ <Form.Item name="customConfig" label={t('Custom config')}>
218
+ <JsonTextArea rows={6} json5 />
219
+ </Form.Item>
220
+ ```
221
+
222
+ Key props:
223
+
224
+ - `space`: serialization indent. Defaults to `2`
225
+ - `json5`: parse with JSON5 (tolerates trailing commas, comments, single quotes, etc.). Defaults to `false`
226
+ - `showError`: render the parse error inline below the textarea. Defaults to `true`
227
+ - All other antd `Input.TextArea` props are passed through
228
+
229
+ `value` / `onChange` are typed as `unknown` because JSON values can be any shape. Tighten the contract with validators in `Form.Item.rules`.
230
+
231
+ ### Data table
232
+
233
+ #### Table
234
+
235
+ The standard settings-page table, built on antd `Table` with two additions:
236
+
237
+ 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
238
+ 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
239
+
240
+ ```tsx
241
+ import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';
242
+
243
+ <Table<AuthenticatorRecord>
244
+ rowKey="id"
245
+ loading={loading}
246
+ columns={columns}
247
+ dataSource={data?.records || []}
248
+ isDraggable
249
+ onSortEnd={async (from, to) => {
250
+ await resource.move({ sourceId: from.id, targetId: to.id });
251
+ refresh();
252
+ }}
253
+ rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
254
+ pagination={{
255
+ current: page,
256
+ pageSize,
257
+ total: data?.total || 0,
258
+ onChange: (next, nextSize) => { /* ... */ },
259
+ }}
260
+ />
261
+ ```
262
+
263
+ Key props:
264
+
265
+ - `rowKey`: required. Drag-sort and row-identity both depend on it
266
+ - `showIndex`: defaults to `true`; disable to keep the cell at checkbox-only
267
+ - `isDraggable`: drag-and-drop toggle. Defaults to `false` — when off the component is a thin antd `Table` superset
268
+ - `onSortEnd: (from, to) => void | Promise`: fired when a row is dropped. Caller persists
269
+ - `showSortHandle`: defaults to `true`; set false when you want the handle off (or embedded into a custom column via `<SortHandle />`)
270
+ - All other antd `Table` props are passed through
271
+
272
+ Companion exports:
273
+
274
+ - `DEFAULT_PAGE_SIZE` (value `50`): suggested default page size
275
+ - `PAGE_SIZE_OPTIONS`: suggested page-size dropdown values `[5, 10, 20, 50, 100, 200]`
276
+ - `SortHandle`: standalone handle component, exported from `@nocobase/client-v2` for embedding into custom columns
277
+
278
+ ### Filter
279
+
280
+ #### CollectionFilter
281
+
282
+ Filter button bound to a Collection. Clicking opens a Popover hosting a multi-condition filter form (field picker + operator + value control). Submit dismisses the Popover and emits the compiled NocoBase filter via `onChange`; Reset keeps the Popover open and emits `undefined`.
283
+
284
+ ```tsx
285
+ import { CollectionFilter, ExtendCollectionsProvider } from '@nocobase/client-v2';
286
+ import lockedUsersCollection from '../../collections/locked-users';
287
+
288
+ function Page() {
289
+ const main = engine.context.dataSourceManager?.getDataSource?.('main');
290
+ const collection = main?.getCollection?.(lockedUsersCollection.name);
291
+
292
+ const listRequest = useRequest(
293
+ async (filter) => api.resource('lockedUsers').list({ ...(filter ? { filter } : {}) }),
294
+ { defaultParams: [undefined] },
295
+ );
296
+
297
+ return (
298
+ <ExtendCollectionsProvider collections={[lockedUsersCollection]}>
299
+ <CollectionFilter collection={collection} onChange={listRequest.run} t={t} />
300
+ {/* table … */}
301
+ </ExtendCollectionsProvider>
302
+ );
303
+ }
304
+ ```
305
+
306
+ Key props:
307
+
308
+ - `collection`: the Collection that drives the field picker. The button is disabled while it's `undefined`
309
+ - `onChange: (filter) => void`: fired on Submit and Reset with the compiled NocoBase filter (`undefined` on Reset). Most pages forward straight to `listRequest.run`
310
+ - `t`: translator. Pass `useT()` from a plugin's `locale.ts` so server-side `{{t("…")}}` macros in field / operator labels get expanded — plain react-i18next's `t` leaves them as literal template strings
311
+ - `filterableFieldNames`: whitelist of root-level field names to expose
312
+ - `noIgnore`: bypass the whitelist
313
+ - `buttonText`: override the trigger label; defaults to `t('Filter')`
314
+ - `showCount`: show the `(N)` condition-count badge on the trigger; defaults to `true`
315
+ - `popoverProps` / `buttonProps`: pass-through to the antd `Popover` / `Button`
316
+ - `popoverMinWidth`: min-width of the popover body; defaults to `520`
317
+
318
+ If the target Collection is `schema-only` (not auto-published from the server to the v2 data source), wrap the page in `<ExtendCollectionsProvider>` so `CollectionFilter` can resolve it by name.
319
+
320
+ ### Utilities
321
+
322
+ #### createFormRegistry
323
+
324
+ Factory for a namespaced "entry registry". Each call returns an independent registry instance backed by its own closure `Map`.
325
+
326
+ ```ts
327
+ import { createFormRegistry, type FormRegistryEntry } from '@nocobase/client-v2';
328
+
329
+ interface StorageType extends FormRegistryEntry {
330
+ // FormRegistryEntry requires at least `name: string`
331
+ title: string;
332
+ Component: React.ComponentType;
333
+ }
334
+
335
+ const storageTypes = createFormRegistry<StorageType>('file-manager/storage-types');
336
+
337
+ storageTypes.register({ name: 'local', title: 'Local storage', Component: LocalStorageForm });
338
+ storageTypes.register({ name: 's3', title: 'Amazon S3', Component: S3StorageForm });
339
+
340
+ storageTypes.get('s3');
341
+ storageTypes.list();
342
+ storageTypes.has('local');
343
+ storageTypes.unregister('local');
344
+ ```
345
+
346
+ 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.
347
+
348
+ Re-registering the same `name` overwrites the previous entry and emits a `console.warn` — HMR doesn't throw, and unintended duplicates surface in dev.
349
+
350
+ ## data-source/
351
+
352
+ Components that wire collections / data sources into the React tree. Exported from the top level of `@nocobase/client-v2`.
353
+
354
+ ### ExtendCollectionsProvider
355
+
356
+ Mount-scoped collection injector. On mount it registers the given collections into the target data source; on unmount it removes them. A `dataSource:loaded` listener re-applies the registration so mid-session reloads don't wipe injected collections.
357
+
358
+ ```tsx
359
+ import { ExtendCollectionsProvider } from '@nocobase/client-v2';
360
+ import lockedUsersCollection from '../../collections/locked-users';
361
+
362
+ // Module-level constant — keeps the reference stable so the provider's
363
+ // effect doesn't re-run on every parent re-render.
364
+ const collections = [lockedUsersCollection];
365
+
366
+ export function LockedUsersPage() {
367
+ return (
368
+ <ExtendCollectionsProvider collections={collections}>
369
+ <LockedUsersPageInner />
370
+ </ExtendCollectionsProvider>
371
+ );
372
+ }
373
+ ```
374
+
375
+ Key props:
376
+
377
+ - `collections: CollectionOptions[]`: collections to inject. The provider only adds names that aren't already present, and on unmount removes only the ones it added
378
+ - `dataSource`: target data source key; defaults to `'main'`
379
+ - `children`: subtree covered by the injection
380
+
381
+ When to use:
382
+
383
+ - The server-side collection is `schema-only` and doesn't get auto-published to the client data source (e.g. `lockedUsers`)
384
+ - You need a client-side mirror that should be visible only inside the current page, not registered globally
385
+
386
+ Typical pairing: use together with `<CollectionFilter>` — the provider makes the collection resolvable; the filter button consumes it.
387
+
388
+ ## When to add a new component here
389
+
390
+ - Two or more plugins need the same field or container shape — promote it to this folder
391
+ - 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/`
392
+ - 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
393
+
394
+ Two follow-ups after adding a new component:
395
+
396
+ 1. Add `export * from './XxxComponent'` to `form/index.tsx`
397
+ 2. Document it here so the next plugin migration finds it