@nocobase/client-v2 2.1.0-alpha.39 → 2.1.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/BaseApplication.d.ts +1 -1
- package/es/components/PoweredBy.d.ts +18 -0
- package/es/components/SwitchLanguage.d.ts +11 -0
- package/es/components/form/DialogFormLayout.d.ts +75 -0
- package/es/components/form/DrawerFormLayout.d.ts +11 -11
- package/es/components/form/PasswordInput.d.ts +40 -0
- package/es/components/form/RemoteSelect.d.ts +79 -0
- package/es/components/form/index.d.ts +3 -0
- package/es/components/form/table/styles.d.ts +10 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/models/base/ActionModelCore.d.ts +6 -0
- package/es/flow/models/base/GridModel.d.ts +2 -0
- package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/hooks/index.d.ts +2 -0
- package/es/hooks/useCurrentAppInfo.d.ts +9 -0
- package/es/index.mjs +102 -90
- package/es/nocobase-buildin-plugin/index.d.ts +25 -0
- package/es/utils/appVersionHTML.d.ts +10 -0
- package/es/utils/index.d.ts +1 -0
- package/es/utils/remotePlugins.d.ts +4 -1
- package/lib/index.js +108 -96
- package/package.json +7 -7
- package/src/BaseApplication.tsx +3 -3
- package/src/PluginSettingsManager.ts +2 -1
- package/src/__tests__/PluginSettingsManager.test.ts +19 -0
- package/src/__tests__/PoweredBy.test.tsx +130 -0
- package/src/__tests__/app.test.tsx +31 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
- package/src/__tests__/remotePlugins.test.ts +55 -0
- package/src/__tests__/useCurrentRoles.test.tsx +100 -0
- package/src/components/PoweredBy.tsx +71 -0
- package/src/components/README.md +314 -0
- package/src/components/README.zh-CN.md +312 -0
- package/src/components/SwitchLanguage.tsx +48 -0
- package/src/components/form/DialogFormLayout.tsx +111 -0
- package/src/components/form/DrawerFormLayout.tsx +13 -32
- package/src/components/form/PasswordInput.tsx +211 -0
- package/src/components/form/RemoteSelect.tsx +137 -0
- package/src/components/form/index.tsx +3 -0
- package/src/components/form/table/Table.tsx +2 -1
- package/src/components/form/table/styles.ts +19 -0
- package/src/components/index.ts +2 -0
- package/src/css-variable/CSSVariableProvider.tsx +10 -1
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
- package/src/flow/actions/dataScope.tsx +3 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/models/base/ActionModel.tsx +8 -7
- package/src/flow/models/base/ActionModelCore.tsx +15 -7
- package/src/flow/models/base/GridModel.tsx +93 -36
- package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
- package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
- package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useCurrentAppInfo.ts +36 -0
- package/src/nocobase-buildin-plugin/index.tsx +70 -16
- package/src/utils/appVersionHTML.ts +28 -0
- package/src/utils/globalDeps.ts +2 -2
- package/src/utils/index.tsx +2 -0
- package/src/utils/remotePlugins.ts +12 -7
|
@@ -0,0 +1,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;
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
/**
|
|
20
|
+
* Called before the dialog is closed by the Cancel button or the
|
|
21
|
+
* top-right close (X) icon. Use for "discard changes" confirmations.
|
|
22
|
+
*/
|
|
23
|
+
onCancel?: () => void | Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Called when the Submit button is clicked. Caller owns validation
|
|
26
|
+
* + the actual API call; the dialog is closed automatically when
|
|
27
|
+
* `onSubmit` resolves. Throw from `onSubmit` to keep the dialog open
|
|
28
|
+
* (e.g. on a validation error).
|
|
29
|
+
*/
|
|
30
|
+
onSubmit?: () => void | Promise<void>;
|
|
31
|
+
/** Drives the Submit button's loading state. */
|
|
32
|
+
submitting?: boolean;
|
|
33
|
+
/** Override the Submit button label. Defaults to "Submit". */
|
|
34
|
+
submitText?: React.ReactNode;
|
|
35
|
+
/** Override the Cancel button label. Defaults to "Cancel". */
|
|
36
|
+
cancelText?: React.ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Full override of the footer content. When provided, the default
|
|
39
|
+
* Cancel + Submit buttons are replaced. Useful for forms that need
|
|
40
|
+
* extra actions (e.g. Preview, Save draft).
|
|
41
|
+
*/
|
|
42
|
+
footer?: React.ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Standard layout for dialog-hosted forms — the dialog counterpart of
|
|
47
|
+
* `DrawerFormLayout`. Title sits left-aligned in the dialog's native
|
|
48
|
+
* header (no inline close icon — the dialog provides its own X in the
|
|
49
|
+
* top-right when opened with `viewer.dialog({ closable: true, ... })`),
|
|
50
|
+
* the form body fills the middle, and a Cancel + Submit footer sits
|
|
51
|
+
* at the bottom.
|
|
52
|
+
*
|
|
53
|
+
* Why not just reuse `DrawerFormLayout`? `DrawerFormLayout` injects a
|
|
54
|
+
* `<CloseOutlined>` button next to the title — that's the drawer
|
|
55
|
+
* visual contract (close lives near the title in a side panel). In a
|
|
56
|
+
* centered dialog the native top-right close button is the expected
|
|
57
|
+
* affordance, so a separate layout keeps the visual contract clean.
|
|
58
|
+
*
|
|
59
|
+
* Callers own the `<Form>` instance, validation, and the actual API
|
|
60
|
+
* call. This component only handles the chrome and close behaviour.
|
|
61
|
+
*
|
|
62
|
+
* Example:
|
|
63
|
+
*
|
|
64
|
+
* ```tsx
|
|
65
|
+
* ctx.viewer.dialog({
|
|
66
|
+
* closable: true, // native top-right X
|
|
67
|
+
* content: () => (
|
|
68
|
+
* <DialogFormLayout
|
|
69
|
+
* title={t('Bind verifier')}
|
|
70
|
+
* onSubmit={handleSubmit}
|
|
71
|
+
* submitting={submitting}
|
|
72
|
+
* submitText={t('Bind')}
|
|
73
|
+
* >
|
|
74
|
+
* <Form form={form} layout="vertical">...</Form>
|
|
75
|
+
* </DialogFormLayout>
|
|
76
|
+
* ),
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function DialogFormLayout(props: DialogFormLayoutProps) {
|
|
81
|
+
const view = useFlowView();
|
|
82
|
+
|
|
83
|
+
const handleCancel = useCallback(async () => {
|
|
84
|
+
await props.onCancel?.();
|
|
85
|
+
await view.close();
|
|
86
|
+
}, [props, view]);
|
|
87
|
+
|
|
88
|
+
const handleSubmit = useCallback(async () => {
|
|
89
|
+
await props.onSubmit?.();
|
|
90
|
+
await view.close();
|
|
91
|
+
}, [props, view]);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div>
|
|
95
|
+
{view.Header ? <view.Header title={props.title} /> : null}
|
|
96
|
+
{props.children}
|
|
97
|
+
{view.Footer ? (
|
|
98
|
+
<view.Footer>
|
|
99
|
+
{props.footer ?? (
|
|
100
|
+
<Space>
|
|
101
|
+
<Button onClick={handleCancel}>{props.cancelText ?? 'Cancel'}</Button>
|
|
102
|
+
<Button type="primary" loading={props.submitting} onClick={handleSubmit}>
|
|
103
|
+
{props.submitText ?? 'Submit'}
|
|
104
|
+
</Button>
|
|
105
|
+
</Space>
|
|
106
|
+
)}
|
|
107
|
+
</view.Footer>
|
|
108
|
+
) : null}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -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
|
|
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
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
}, [
|
|
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>
|