@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.
Files changed (67) hide show
  1. package/es/BaseApplication.d.ts +1 -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 +75 -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/index.d.ts +3 -0
  9. package/es/components/form/table/styles.d.ts +10 -0
  10. package/es/components/index.d.ts +2 -0
  11. package/es/flow/models/base/ActionModelCore.d.ts +6 -0
  12. package/es/flow/models/base/GridModel.d.ts +2 -0
  13. package/es/flow/utils/dataScopeFormValueClear.d.ts +14 -0
  14. package/es/flow-compat/passwordUtils.d.ts +1 -1
  15. package/es/hooks/index.d.ts +2 -0
  16. package/es/hooks/useCurrentAppInfo.d.ts +9 -0
  17. package/es/index.mjs +102 -90
  18. package/es/nocobase-buildin-plugin/index.d.ts +25 -0
  19. package/es/utils/appVersionHTML.d.ts +10 -0
  20. package/es/utils/index.d.ts +1 -0
  21. package/es/utils/remotePlugins.d.ts +4 -1
  22. package/lib/index.js +108 -96
  23. package/package.json +7 -7
  24. package/src/BaseApplication.tsx +3 -3
  25. package/src/PluginSettingsManager.ts +2 -1
  26. package/src/__tests__/PluginSettingsManager.test.ts +19 -0
  27. package/src/__tests__/PoweredBy.test.tsx +130 -0
  28. package/src/__tests__/app.test.tsx +31 -0
  29. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +39 -72
  30. package/src/__tests__/remotePlugins.test.ts +55 -0
  31. package/src/__tests__/useCurrentRoles.test.tsx +100 -0
  32. package/src/components/PoweredBy.tsx +71 -0
  33. package/src/components/README.md +314 -0
  34. package/src/components/README.zh-CN.md +312 -0
  35. package/src/components/SwitchLanguage.tsx +48 -0
  36. package/src/components/form/DialogFormLayout.tsx +111 -0
  37. package/src/components/form/DrawerFormLayout.tsx +13 -32
  38. package/src/components/form/PasswordInput.tsx +211 -0
  39. package/src/components/form/RemoteSelect.tsx +137 -0
  40. package/src/components/form/index.tsx +3 -0
  41. package/src/components/form/table/Table.tsx +2 -1
  42. package/src/components/form/table/styles.ts +19 -0
  43. package/src/components/index.ts +2 -0
  44. package/src/css-variable/CSSVariableProvider.tsx +10 -1
  45. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +96 -0
  46. package/src/flow/actions/dataScope.tsx +3 -0
  47. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -4
  48. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +7 -33
  49. package/src/flow/components/BlockItemCard.tsx +2 -2
  50. package/src/flow/models/base/ActionModel.tsx +8 -7
  51. package/src/flow/models/base/ActionModelCore.tsx +15 -7
  52. package/src/flow/models/base/GridModel.tsx +93 -36
  53. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +83 -11
  54. package/src/flow/models/blocks/details/DetailsItemModel.tsx +2 -0
  55. package/src/flow/models/blocks/form/FormItemModel.tsx +5 -3
  56. package/src/flow/models/blocks/form/__tests__/FormItemModel.defineChildren.test.ts +108 -0
  57. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +5 -0
  58. package/src/flow/models/blocks/table/TableColumnModel.tsx +2 -0
  59. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +2 -0
  60. package/src/flow/utils/dataScopeFormValueClear.ts +278 -0
  61. package/src/hooks/index.ts +2 -0
  62. package/src/hooks/useCurrentAppInfo.ts +36 -0
  63. package/src/nocobase-buildin-plugin/index.tsx +70 -16
  64. package/src/utils/appVersionHTML.ts +28 -0
  65. package/src/utils/globalDeps.ts +2 -2
  66. package/src/utils/index.tsx +2 -0
  67. 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,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