@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,394 @@
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({ closable: true, content })` 用。
18
+
19
+ - 顶部 Header:只放标题;左侧的关闭 X 来自 antd Drawer——必须在 `viewer.drawer` 上显式传 `closable: true` 才会出现
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
+ closable: true, // 关键:开启 antd Drawer 原生关闭 X
29
+ content: () => (
30
+ <DrawerFormLayout
31
+ title={t('添加认证器')}
32
+ onSubmit={handleSubmit}
33
+ submitting={submitting}
34
+ >
35
+ <Form form={form} layout="vertical">
36
+ {/* 字段 */}
37
+ </Form>
38
+ </DrawerFormLayout>
39
+ ),
40
+ });
41
+ ```
42
+
43
+ 主要属性:
44
+
45
+ - `title`:标题节点
46
+ - `onSubmit`:回调,resolve 后会自动关闭抽屉。throw 可以让抽屉保持打开(比如校验失败)
47
+ - `submitting`:驱动 Submit 按钮的 loading
48
+ - `submitText` / `cancelText`:按钮文字
49
+ - `footer`:完全自定义 Footer 内容(覆盖默认两个按钮)
50
+
51
+ 需要在关闭前做「未保存改动」之类的确认,用更底层的 `viewer.drawer({ preventClose, beforeClose })`,这层 layout 不再包装 cancel 拦截。
52
+
53
+ #### DialogFormLayout
54
+
55
+ 弹窗形态的表单 layout,跟 `DrawerFormLayout` 同形。配合 `ctx.viewer.dialog({ closable: true, content })` 用。
56
+
57
+ 视觉上的差异只有关闭 X 的位置——Drawer 是 antd Drawer 自带的左上角 X,Dialog 是 antd Modal 自带的右上角 X。两边都依赖在 viewer 调用处显式传 `closable: true`,layout 自己都不渲染 close 图标。
58
+
59
+ ```tsx
60
+ import { DialogFormLayout } from '@nocobase/client-v2';
61
+
62
+ ctx.viewer.dialog({
63
+ closable: true, // 关键:开启 antd Modal 原生右上角 X
64
+ content: () => (
65
+ <DialogFormLayout title={t('绑定验证码')} onSubmit={handleSubmit}>
66
+ <Form form={form} layout="vertical">
67
+ {/* 字段 */}
68
+ </Form>
69
+ </DialogFormLayout>
70
+ ),
71
+ });
72
+ ```
73
+
74
+ 什么时候选哪个?
75
+
76
+ - **Drawer**:长表单、字段多、需要从一侧滑出占用整面(比如设置页的「添加 / 编辑」)
77
+ - **Dialog**:短表单、需要快速确认(比如绑定、修改密码、二次验证)
78
+
79
+ 属性跟 `DrawerFormLayout` 基本一致,可以直接换。唯一区别:`DialogFormLayout` 多一个 `onCancel` 回调(Cancel 按钮和原生 X 都会触发),用于「丢弃改动」之类的确认。
80
+
81
+ ### 表单字段
82
+
83
+ #### RemoteSelect
84
+
85
+ 异步拉数据的 Select。框架级组件——不感知 NocoBase 业务,调用方传一个 `request` 函数自己拉数据。
86
+
87
+ ```tsx
88
+ import { RemoteSelect } from '@nocobase/client-v2';
89
+
90
+ <Form.Item name="provider" label={t('服务商')}>
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
+ 主要属性:
103
+
104
+ - `request: () => Promise`:拉数据,必填。可以返回数组,也可以返回带元数据的对象(搭配 `selectItems` 取出数组)
105
+ - `selectItems`:从 `request` 返回值中抽出数组的函数。响应体是 `{ items, meta }` 形态时用
106
+ - `fieldNames`:默认按 `{ label, value }` 字段映射;不匹配的时候用 `mapOptions` 完全自定义
107
+ - `mapOptions: (item, index) => ({ label, value })`:完整覆盖映射逻辑
108
+ - `cacheKey` / `refreshDeps` / `ready`:透传给 ahooks `useRequest`,控制缓存和 refresh 时机
109
+ - `onLoaded: (items, response) => void`:拉到数据后的回调,能拿到原始响应
110
+
111
+ 剩下的 antd `Select` props(`mode` / `placeholder` / `disabled` / `value` / `onChange` 等)都原样透传。
112
+
113
+ 默认开启 `showSearch` + `allowClear`,搜索是本地模式(按 label 匹配)。要做服务端搜索,把搜索词放进 `refreshDeps`,在 `request` 里读出来发请求。
114
+
115
+ #### EnvVariableInput
116
+
117
+ `$env` 命名空间的变量输入器。专门给「密钥 / 凭证」这类字段用——支持环境变量引用,同时给纯字面值加 password mask。
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
+ 主要属性:
128
+
129
+ - `password`:开启后,非变量的字面值会用 `Input.Password` 形态遮盖。变量表达式(比如 `{{ $env.X }}`)依然可见可编辑
130
+ - `placeholder` / `disabled` / `value` / `onChange`:标准受控字段属性
131
+
132
+ 值的形态是字符串:`'literal'` 或 `'{{ $env.foo.bar }}'`。服务端在使用时再展开成实际值。
133
+
134
+ #### VariableInput / VariableTextArea
135
+
136
+ 通用变量输入器。可以引用任意 `flowEngine.context` 注册的命名空间(`$env` / `$user` / 业务自定义的 `$resetLink` 等)。
137
+
138
+ 两者差异:
139
+
140
+ - `VariableInput`:单行,变量渲染成彩色 pill(视觉上是「短标签」)
141
+ - `VariableTextArea`:多行,变量保留 `{{ ... }}` 字面——适合邮件模板这种「字面 + 变量」混排的长文本
142
+
143
+ ```tsx
144
+ import { VariableInput, VariableTextArea } from '@nocobase/client-v2';
145
+
146
+ // 邮件主题:单行 pill 形态
147
+ <Form.Item name={['options', 'emailSubject']} label={t('主题')}>
148
+ <VariableInput
149
+ namespaces={['$env']}
150
+ extraNodes={[
151
+ { name: '$resetLink', title: t('重置密码链接'), type: 'string', paths: ['$resetLink'] },
152
+ ]}
153
+ />
154
+ </Form.Item>
155
+
156
+ // 邮件正文:多行字面
157
+ <Form.Item name={['options', 'emailContentHTML']} label={t('正文')}>
158
+ <VariableTextArea namespaces={['$env']} rows={10} />
159
+ </Form.Item>
160
+ ```
161
+
162
+ 主要属性:
163
+
164
+ - `namespaces`:限定可选的顶层命名空间。不传就用 `flowEngine.context` 里全部已注册的
165
+ - `extraNodes`:在命名空间过滤后追加几条静态变量(用于 `$resetLink` 这类只在当前页面有意义的局部变量)
166
+ - `converters`:覆盖默认的 path ↔ string 转换器。`EnvVariableInput` 就是用这个钩子把输出锁定到 `$env`
167
+ - `value` / `onChange` / `placeholder` / `disabled`:标准受控字段属性
168
+
169
+ 底层共用 `VariableHybridInput`(`VariableInput`)和 `TextAreaWithContextSelector`(`VariableTextArea`),用同一套 MetaTree 数据。
170
+
171
+ #### FileSizeInput
172
+
173
+ 文件大小输入器。值统一存字节数,UI 上配一个单位选择器(Byte / KB / MB / GB)。
174
+
175
+ ```tsx
176
+ import { FileSizeInput } from '@nocobase/client-v2';
177
+
178
+ <Form.Item name="maxFileSize" label={t('单文件大小上限')}>
179
+ <FileSizeInput min={1} max={1024 * 1024 * 1024} defaultValue={20 * 1024 * 1024} />
180
+ </Form.Item>
181
+ ```
182
+
183
+ 主要属性:
184
+
185
+ - `min` / `max`:允许的字节数区间,blur 时会自动 clamp 回界内。默认 `min=1`、`max=Infinity`
186
+ - `defaultValue`:用来决定初次显示的单位(比如默认 20 MB 就会以 MB 单位起始)
187
+ - `value` / `onChange`:受控字段,值类型是 `number`(字节)
188
+
189
+ #### PasswordInput
190
+
191
+ antd `Input.Password` 加一个可选的强度提示条,从 v1 的 `Password` 组件移植过来。用于「设置 / 修改密码」类表单——v1 → v2 迁移过来后视觉信号保持一致。
192
+
193
+ ```tsx
194
+ import { PasswordInput } from '@nocobase/client-v2';
195
+
196
+ <Form.Item name="newPassword" label={t('新密码')} rules={[{ required: true }]}>
197
+ <PasswordInput autoComplete="new-password" checkStrength />
198
+ </Form.Item>
199
+ ```
200
+
201
+ 主要属性:
202
+
203
+ - `checkStrength`:在输入框下方渲染一条强度提示。默认 `false`。强度评分按 `[20, 40, 60, 80, 100]` 分桶,用裁剪的橙色渐变填充在灰色底条上,配色跟 v1 保持一致
204
+ - 其他 antd `Input.Password` 属性原样透传:`value` / `onChange` / `disabled` / `placeholder` / `autoComplete` 等
205
+
206
+ 强度条只是 UX 提示,**不是表单校验**。弱密码仍然能提交,除非 server(或单独安装的 password-policy 商业插件)拒绝。真正的密码规则通过 `Form.Item.rules` 或——等开源 ↔ 商业的 extension point 落地之后——项目共享的 password validator hook 接入。
207
+
208
+ #### JsonTextArea
209
+
210
+ JSON 输入器。存的值是 JS 对象(不是字符串),编辑时实时解析、blur 时校验。
211
+
212
+ ```tsx
213
+ import { JsonTextArea } from '@nocobase/client-v2';
214
+
215
+ <Form.Item name="customConfig" label={t('自定义配置')}>
216
+ <JsonTextArea rows={6} json5 />
217
+ </Form.Item>
218
+ ```
219
+
220
+ 主要属性:
221
+
222
+ - `space`:序列化缩进,默认 `2`
223
+ - `json5`:开启后用 JSON5 解析(容忍尾逗号、注释、单引号等)。默认关
224
+ - `showError`:解析失败时是否在下方显示错误消息。默认 `true`
225
+ - 其他 antd `Input.TextArea` 的属性都透传
226
+
227
+ `value` / `onChange` 的类型是 `unknown`——因为 JSON 可以是任意结构。调用方按业务约束在 `Form.Item.rules` 里加 validator 收紧类型。
228
+
229
+ ### 数据表
230
+
231
+ #### Table
232
+
233
+ 设置页表格的标准组件,基于 antd `Table` 扩展了两点:
234
+
235
+ 1. **行索引和复选框切换**:默认状态显示「1 / 2 / 3」行号,悬停或选中时切换成 checkbox。两个元素绝对定位在同一格内,不会抢空间。需要 `rowSelection` 才生效
236
+ 2. **拖拽排序**:传 `isDraggable` 开启,每行左侧出现拖拽手柄;放下时触发 `onSortEnd`。组件不动 `dataSource`,由调用方在回调里跑 `resource.move(...)` 再 `refresh()`
237
+
238
+ ```tsx
239
+ import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';
240
+
241
+ <Table<AuthenticatorRecord>
242
+ rowKey="id"
243
+ loading={loading}
244
+ columns={columns}
245
+ dataSource={data?.records || []}
246
+ isDraggable
247
+ onSortEnd={async (from, to) => {
248
+ await resource.move({ sourceId: from.id, targetId: to.id });
249
+ refresh();
250
+ }}
251
+ rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
252
+ pagination={{
253
+ current: page,
254
+ pageSize,
255
+ total: data?.total || 0,
256
+ onChange: (next, nextSize) => { /* ... */ },
257
+ }}
258
+ />
259
+ ```
260
+
261
+ 主要属性:
262
+
263
+ - `rowKey`:必填。拖拽和行身份识别都依赖它
264
+ - `showIndex`:默认 `true`,关掉就只显示 checkbox
265
+ - `isDraggable`:开关拖拽。默认 `false`,关掉就是个加强版 antd Table
266
+ - `onSortEnd: (from, to) => void | Promise`:拖拽放下时触发。调用方负责持久化
267
+ - `showSortHandle`:默认 `true`,需要时可以隐藏手柄,自己在某列里嵌 `<SortHandle />`
268
+ - 其他 antd `Table` props 全部透传
269
+
270
+ 附带导出:
271
+
272
+ - `DEFAULT_PAGE_SIZE`(值 `50`):建议的默认分页大小
273
+ - `PAGE_SIZE_OPTIONS`:建议的分页选项 `[5, 10, 20, 50, 100, 200]`
274
+ - `SortHandle`:从 `@nocobase/client-v2` 导出的独立手柄组件,可以嵌进自定义列
275
+
276
+ ### 筛选
277
+
278
+ #### CollectionFilter
279
+
280
+ 绑定 Collection 的筛选按钮。点击展开 Popover,里面是多条件筛选表单(字段选择器 + 操作符 + 取值控件)。Submit 收起 Popover 并通过 `onChange` 发出 NocoBase filter 参数;Reset 保持 Popover 打开并发出 `undefined`。
281
+
282
+ ```tsx
283
+ import { CollectionFilter, ExtendCollectionsProvider } from '@nocobase/client-v2';
284
+ import lockedUsersCollection from '../../collections/locked-users';
285
+
286
+ function Page() {
287
+ const main = engine.context.dataSourceManager?.getDataSource?.('main');
288
+ const collection = main?.getCollection?.(lockedUsersCollection.name);
289
+
290
+ const listRequest = useRequest(
291
+ async (filter) => api.resource('lockedUsers').list({ ...(filter ? { filter } : {}) }),
292
+ { defaultParams: [undefined] },
293
+ );
294
+
295
+ return (
296
+ <ExtendCollectionsProvider collections={[lockedUsersCollection]}>
297
+ <CollectionFilter collection={collection} onChange={listRequest.run} t={t} />
298
+ {/* table … */}
299
+ </ExtendCollectionsProvider>
300
+ );
301
+ }
302
+ ```
303
+
304
+ 主要属性:
305
+
306
+ - `collection`:作为字段来源的 Collection。`undefined` 时按钮 disabled
307
+ - `onChange: (filter) => void`:Submit 或 Reset 时触发,参数是编译好的 NocoBase filter(Reset 时为 `undefined`)。常见做法是直接转给 `listRequest.run`
308
+ - `t`:翻译函数。建议传 `useT()`(来自插件 `locale.ts`),它会自动展开服务端返回的 `{{t("…")}}` 模板,否则字段标签、操作符标签可能显示成字面模板
309
+ - `filterableFieldNames`:白名单,限制顶层可选字段
310
+ - `noIgnore`:忽略白名单
311
+ - `buttonText`:覆盖按钮文字,默认 `t('Filter')`
312
+ - `showCount`:是否在按钮上显示当前条件数 `(N)`,默认 `true`
313
+ - `popoverProps` / `buttonProps`:透传给 antd `Popover` / `Button`
314
+ - `popoverMinWidth`:Popover 内容最小宽度,默认 `520`
315
+
316
+ 要筛选的 Collection 如果是 `schema-only`(服务端没自动发布到客户端 data source),用 `<ExtendCollectionsProvider>` 包一下当前页面,让 `CollectionFilter` 能解析到。
317
+
318
+ ### 工具
319
+
320
+ #### createFormRegistry
321
+
322
+ 带命名空间的「条目注册表」工厂。每次调用返回一个独立的 registry 实例,闭包持有自己的 `Map`。
323
+
324
+ ```ts
325
+ import { createFormRegistry, type FormRegistryEntry } from '@nocobase/client-v2';
326
+
327
+ interface StorageType extends FormRegistryEntry {
328
+ // FormRegistryEntry 要求至少有 `name: string`
329
+ title: string;
330
+ Component: React.ComponentType;
331
+ }
332
+
333
+ const storageTypes = createFormRegistry<StorageType>('file-manager/storage-types');
334
+
335
+ storageTypes.register({ name: 'local', title: '本地存储', Component: LocalStorageForm });
336
+ storageTypes.register({ name: 's3', title: 'Amazon S3', Component: S3StorageForm });
337
+
338
+ storageTypes.get('s3');
339
+ storageTypes.list();
340
+ storageTypes.has('local');
341
+ storageTypes.unregister('local');
342
+ ```
343
+
344
+ 主要用在:插件需要给「同名 + 同形 + 不同实现」的东西做扩展点(比如 file-manager 的存储类型、verification 的 OTP provider)。比 `Map` 多了 namespace 标识和 HMR 友好的覆盖警告。
345
+
346
+ `name` 重复注册会用新条目覆盖旧的,同时打 `console.warn`——HMR 时不抛错,开发期能看到意外的重复。
347
+
348
+ ## data-source/
349
+
350
+ 跟数据源 / Collection 注册相关的组件。从 `@nocobase/client-v2` 顶层 export。
351
+
352
+ ### ExtendCollectionsProvider
353
+
354
+ 挂载期 Collection 注入器。在组件挂载时把传入的 collection 注册到目标 data source,卸载时移除;会监听 `dataSource:loaded` 自动重新注入,确保数据源 reload 时不会被清掉。
355
+
356
+ ```tsx
357
+ import { ExtendCollectionsProvider } from '@nocobase/client-v2';
358
+ import lockedUsersCollection from '../../collections/locked-users';
359
+
360
+ // 模块级常量——保证引用稳定,避免 provider 每次父级重渲染都重跑 effect
361
+ const collections = [lockedUsersCollection];
362
+
363
+ export function LockedUsersPage() {
364
+ return (
365
+ <ExtendCollectionsProvider collections={collections}>
366
+ <LockedUsersPageInner />
367
+ </ExtendCollectionsProvider>
368
+ );
369
+ }
370
+ ```
371
+
372
+ 主要属性:
373
+
374
+ - `collections: CollectionOptions[]`:本次要注入的 Collection。Provider 只会注册当时不存在的那些,卸载时也只移除自己注册过的
375
+ - `dataSource`:目标 data source key,默认 `'main'`
376
+ - `children`:被注入 Collection 覆盖的子树
377
+
378
+ 什么时候用:
379
+
380
+ - 服务端 collection 是 `schema-only`,不会自动发布到客户端 data source(比如 `lockedUsers`)
381
+ - 需要一个纯客户端的 collection 镜像,只对当前页面有效,不污染全局
382
+
383
+ 常见搭配:跟 `<CollectionFilter>` 一起用——前者把 collection 挂上,后者读取并渲染筛选表单。
384
+
385
+ ## 怎么决定加不加新组件
386
+
387
+ - 出现两个及以上插件需要同一形态的字段或容器——抽到这里
388
+ - 跨插件复用、但耦合到具体业务领域(比如「选一个 verifier」「选一个数据源」)——留在业务插件里,从插件的 `client-v2/` 自己 export
389
+ - 抽象前先看现有组件能不能改进:比如 `RemoteSelect` 的 `selectItems` 就是为了让带元数据的响应不需要再开新组件
390
+
391
+ 新增组件后别忘记两件事:
392
+
393
+ 1. 在 `form/index.tsx` 加一行 `export * from './XxxComponent'`
394
+ 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;
@@ -0,0 +1,87 @@
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 { useFlowView } from '@nocobase/flow-engine';
11
+ import { Button, Space } from 'antd';
12
+ import React, { useCallback } from 'react';
13
+
14
+ export interface DialogFormLayoutProps {
15
+ /** Header title rendered in the dialog's title slot. */
16
+ title: React.ReactNode;
17
+ /** Form body — typically a `<Form>` wrapping `<Form.Item>` fields. */
18
+ children: React.ReactNode;
19
+ /** Called before the dialog is closed by the Cancel button or the top-right close (X) icon. Use for "discard changes" confirmations. */
20
+ onCancel?: () => void | Promise<void>;
21
+ /** Called when the Submit button is clicked. Caller owns validation + the actual API call; the dialog is closed automatically when `onSubmit` resolves. Throw from `onSubmit` to keep the dialog open (e.g. on a validation error). */
22
+ onSubmit?: () => void | Promise<void>;
23
+ /** Drives the Submit button's loading state. */
24
+ submitting?: boolean;
25
+ /** Override the Submit button label. Defaults to "Submit". */
26
+ submitText?: React.ReactNode;
27
+ /** Override the Cancel button label. Defaults to "Cancel". */
28
+ cancelText?: React.ReactNode;
29
+ /** Full override of the footer content. When provided, the default Cancel + Submit buttons are replaced. Useful for forms that need extra actions (e.g. Preview, Save draft). */
30
+ footer?: React.ReactNode;
31
+ }
32
+
33
+ /**
34
+ * Standard layout for dialog-hosted forms — the dialog counterpart of `DrawerFormLayout`. Title sits left-aligned in the dialog's native header, the form body fills the middle, and a Cancel + Submit footer sits at the bottom. Neither this layout nor `DrawerFormLayout` renders a close icon — both rely on the caller passing `closable: true` at the `viewer.dialog` / `viewer.drawer` call site to surface antd Modal's native top-right X (Dialog) or antd Drawer's native left-side X (Drawer).
35
+ *
36
+ * Callers own the `<Form>` instance, validation, and the actual API call. This component only handles the chrome and close behaviour.
37
+ *
38
+ * Example:
39
+ *
40
+ * ```tsx
41
+ * ctx.viewer.dialog({
42
+ * closable: true, // native top-right X
43
+ * content: () => (
44
+ * <DialogFormLayout
45
+ * title={t('Bind verifier')}
46
+ * onSubmit={handleSubmit}
47
+ * submitting={submitting}
48
+ * submitText={t('Bind')}
49
+ * >
50
+ * <Form form={form} layout="vertical">...</Form>
51
+ * </DialogFormLayout>
52
+ * ),
53
+ * });
54
+ * ```
55
+ */
56
+ export function DialogFormLayout(props: DialogFormLayoutProps) {
57
+ const view = useFlowView();
58
+
59
+ const handleCancel = useCallback(async () => {
60
+ await props.onCancel?.();
61
+ await view.close();
62
+ }, [props, view]);
63
+
64
+ const handleSubmit = useCallback(async () => {
65
+ await props.onSubmit?.();
66
+ await view.close();
67
+ }, [props, view]);
68
+
69
+ return (
70
+ <div>
71
+ {view.Header ? <view.Header title={props.title} /> : null}
72
+ {props.children}
73
+ {view.Footer ? (
74
+ <view.Footer>
75
+ {props.footer ?? (
76
+ <Space>
77
+ <Button onClick={handleCancel}>{props.cancelText ?? 'Cancel'}</Button>
78
+ <Button type="primary" loading={props.submitting} onClick={handleSubmit}>
79
+ {props.submitText ?? 'Submit'}
80
+ </Button>
81
+ </Space>
82
+ )}
83
+ </view.Footer>
84
+ ) : null}
85
+ </div>
86
+ );
87
+ }
@@ -7,22 +7,15 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { CloseOutlined } from '@ant-design/icons';
11
- import { css } from '@emotion/css';
12
10
  import { useFlowView } from '@nocobase/flow-engine';
13
11
  import { Button, Space } from 'antd';
14
12
  import React, { useCallback } from 'react';
15
13
 
16
14
  export interface DrawerFormLayoutProps {
17
- /** Header title rendered next to the close (X) button. */
15
+ /** Header title rendered next to antd Drawer's native close (X) icon. */
18
16
  title: React.ReactNode;
19
17
  /** Form body — typically a `<Form>` wrapping `<Form.Item>` fields. */
20
18
  children: React.ReactNode;
21
- /**
22
- * Called before the drawer is closed by either the Cancel button or the
23
- * header's X icon. Use for "discard changes" confirmations.
24
- */
25
- onCancel?: () => void | Promise<void>;
26
19
  /**
27
20
  * Called when the Submit button is clicked. Caller owns validation + the
28
21
  * actual API call; the drawer is closed automatically when `onSubmit`
@@ -44,29 +37,26 @@ export interface DrawerFormLayoutProps {
44
37
  footer?: React.ReactNode;
45
38
  }
46
39
 
47
- const titleClassName = css`
48
- display: inline-flex;
49
- align-items: center;
50
- gap: 8px;
51
- margin-left: -8px;
52
- `;
53
-
54
40
  /**
55
- * Standard layout for drawer-hosted forms: a close-icon + title header on
56
- * top, the caller-provided form body in the middle, and a Cancel + Submit
57
- * footer at the bottom. Wraps `useFlowView()`'s `Header` / `Footer` slots
58
- * so the drawer chrome stays consistent across plugins.
41
+ * Standard layout for drawer-hosted forms: a title-only header on top
42
+ * (caller must open the drawer with `viewer.drawer({ closable: true })`
43
+ * so antd Drawer renders its native left-side X next to the title),
44
+ * the caller-provided form body in the middle, and a Cancel + Submit
45
+ * footer at the bottom. Wraps `useFlowView()`'s `Header` / `Footer`
46
+ * slots so the drawer chrome stays consistent across plugins.
47
+ *
48
+ * To intercept close (e.g. dirty-form confirmation), use the lower-level
49
+ * `viewer.drawer({ preventClose, beforeClose, ... })` hooks — this
50
+ * layout no longer wraps a custom close handler.
59
51
  *
60
52
  * Callers own the `<Form>` instance, validation, and the actual API call.
61
- * This component only handles the chrome and the close behaviour.
62
53
  */
63
54
  export function DrawerFormLayout(props: DrawerFormLayoutProps) {
64
55
  const view = useFlowView();
65
56
 
66
57
  const handleCancel = useCallback(async () => {
67
- await props.onCancel?.();
68
58
  await view.close();
69
- }, [props, view]);
59
+ }, [view]);
70
60
 
71
61
  const handleSubmit = useCallback(async () => {
72
62
  await props.onSubmit?.();
@@ -75,16 +65,7 @@ export function DrawerFormLayout(props: DrawerFormLayoutProps) {
75
65
 
76
66
  return (
77
67
  <div>
78
- {view.Header ? (
79
- <view.Header
80
- title={
81
- <span className={titleClassName}>
82
- <Button type="text" size="small" icon={<CloseOutlined />} onClick={handleCancel} />
83
- <span>{props.title}</span>
84
- </span>
85
- }
86
- />
87
- ) : null}
68
+ {view.Header ? <view.Header title={props.title} /> : null}
88
69
  {props.children}
89
70
  {view.Footer ? (
90
71
  <view.Footer>