@nocobase/client-v2 2.1.0-beta.36 → 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.
- package/es/BaseApplication.d.ts +1 -0
- package/es/components/form/DialogFormLayout.d.ts +5 -29
- package/es/components/form/filter/CollectionFilter.d.ts +41 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +41 -0
- package/es/components/form/filter/DateFilterDynamicComponent.d.ts +57 -0
- package/es/components/form/filter/FilterValueInput.d.ts +29 -0
- package/es/components/form/filter/index.d.ts +11 -0
- package/es/components/form/filter/useFilterActionProps.d.ts +96 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/data-source/ExtendCollectionsProvider.d.ts +24 -0
- package/es/data-source/index.d.ts +9 -0
- package/es/flow/components/filter/index.d.ts +2 -0
- package/es/flow/components/filter/useFilterOptions.d.ts +54 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +22 -17
- package/es/nocobase-buildin-plugin/index.d.ts +3 -10
- package/lib/index.js +25 -20
- package/package.json +7 -7
- package/src/BaseApplication.tsx +13 -0
- package/src/__tests__/app.test.tsx +9 -0
- package/src/components/README.md +89 -6
- package/src/components/README.zh-CN.md +89 -7
- package/src/components/form/DialogFormLayout.tsx +5 -29
- package/src/components/form/filter/CollectionFilter.tsx +101 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +176 -0
- package/src/components/form/filter/DateFilterDynamicComponent.tsx +283 -0
- package/src/components/form/filter/FilterValueInput.tsx +198 -0
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +205 -0
- package/src/components/form/filter/__tests__/DateFilterDynamicComponent.test.tsx +148 -0
- package/src/components/form/filter/__tests__/FilterValueInput.test.tsx +243 -0
- package/src/components/form/filter/__tests__/compileFilterGroup.test.ts +146 -0
- package/src/components/form/filter/index.ts +13 -0
- package/src/components/form/filter/useFilterActionProps.ts +200 -0
- package/src/components/form/index.tsx +1 -0
- package/src/data-source/ExtendCollectionsProvider.tsx +70 -0
- package/src/data-source/index.ts +10 -0
- package/src/flow/components/filter/index.ts +3 -0
- package/src/flow/components/filter/useFilterOptions.ts +80 -0
- package/src/index.ts +1 -0
- package/src/nocobase-buildin-plugin/index.tsx +13 -19
- package/src/nocobase-buildin-plugin/plugins/LocalePlugin.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.1.0-beta.
|
|
3
|
+
"version": "2.1.0-beta.37",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"@formily/antd-v5": "1.2.3",
|
|
27
27
|
"@formily/react": "^2.2.27",
|
|
28
28
|
"@formily/shared": "^2.2.27",
|
|
29
|
-
"@nocobase/evaluators": "2.1.0-beta.
|
|
30
|
-
"@nocobase/flow-engine": "2.1.0-beta.
|
|
31
|
-
"@nocobase/sdk": "2.1.0-beta.
|
|
32
|
-
"@nocobase/shared": "2.1.0-beta.
|
|
33
|
-
"@nocobase/utils": "2.1.0-beta.
|
|
29
|
+
"@nocobase/evaluators": "2.1.0-beta.37",
|
|
30
|
+
"@nocobase/flow-engine": "2.1.0-beta.37",
|
|
31
|
+
"@nocobase/sdk": "2.1.0-beta.37",
|
|
32
|
+
"@nocobase/shared": "2.1.0-beta.37",
|
|
33
|
+
"@nocobase/utils": "2.1.0-beta.37",
|
|
34
34
|
"ahooks": "^3.7.2",
|
|
35
35
|
"antd": "5.24.2",
|
|
36
36
|
"antd-style": "3.7.1",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"react-i18next": "^11.15.1",
|
|
45
45
|
"react-router-dom": "^6.30.1"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "7132e5b83ecc0e42b54715eaf1429c72bcef34ae"
|
|
48
48
|
}
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -169,6 +169,18 @@ export abstract class BaseApplication<
|
|
|
169
169
|
return this.wsAuthorized;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
public setDocumentLanguage(language?: string | null) {
|
|
173
|
+
if (typeof document === 'undefined') {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (language) {
|
|
178
|
+
document.documentElement.lang = language;
|
|
179
|
+
} else {
|
|
180
|
+
document.documentElement.removeAttribute('lang');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
172
184
|
constructor(protected options: TOptions = {} as TOptions) {
|
|
173
185
|
this.initRequireJs();
|
|
174
186
|
this.defineObservableState();
|
|
@@ -205,6 +217,7 @@ export abstract class BaseApplication<
|
|
|
205
217
|
this.addRoutes();
|
|
206
218
|
this.i18n.on('languageChanged', (lng) => {
|
|
207
219
|
this.apiClient.auth.locale = lng;
|
|
220
|
+
this.setDocumentLanguage(lng);
|
|
208
221
|
});
|
|
209
222
|
this.initListeners();
|
|
210
223
|
this.afterManagersInitialized();
|
|
@@ -33,6 +33,7 @@ describe('app', () => {
|
|
|
33
33
|
|
|
34
34
|
afterEach(() => {
|
|
35
35
|
document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
|
|
36
|
+
document.documentElement.removeAttribute('lang');
|
|
36
37
|
vi.restoreAllMocks();
|
|
37
38
|
});
|
|
38
39
|
|
|
@@ -89,6 +90,14 @@ describe('app', () => {
|
|
|
89
90
|
expect(favicon.getAttribute('href')).toBe('/favicon/favicon.ico');
|
|
90
91
|
});
|
|
91
92
|
|
|
93
|
+
it('should sync document language when app language changes', async () => {
|
|
94
|
+
const app = new Application({ router });
|
|
95
|
+
|
|
96
|
+
await app.i18n.changeLanguage('ja-JP');
|
|
97
|
+
|
|
98
|
+
expect(document.documentElement.lang).toBe('ja-JP');
|
|
99
|
+
});
|
|
100
|
+
|
|
92
101
|
it('should escape app version html placeholder content', () => {
|
|
93
102
|
expect(getAppVersionHTML('<script>alert(1)</script>&"')).toBe(
|
|
94
103
|
'<span class="nb-app-version">v<script>alert(1)</script>&"</span>',
|
package/src/components/README.md
CHANGED
|
@@ -14,9 +14,9 @@ Grouped by purpose: form containers, form fields, data table, utilities.
|
|
|
14
14
|
|
|
15
15
|
#### DrawerFormLayout
|
|
16
16
|
|
|
17
|
-
Drawer-style form layout. Pair with `ctx.viewer.drawer({ content })`.
|
|
17
|
+
Drawer-style form layout. Pair with `ctx.viewer.drawer({ closable: true, content })`.
|
|
18
18
|
|
|
19
|
-
- Top:
|
|
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
20
|
- Bottom: default Cancel / Submit buttons; override the whole footer with `footer`
|
|
21
21
|
- Middle: caller-supplied `<Form>` instance + fields
|
|
22
22
|
|
|
@@ -25,6 +25,7 @@ import { DrawerFormLayout } from '@nocobase/client-v2';
|
|
|
25
25
|
|
|
26
26
|
ctx.viewer.drawer({
|
|
27
27
|
width: '50%',
|
|
28
|
+
closable: true, // restore antd Drawer's native close X
|
|
28
29
|
content: () => (
|
|
29
30
|
<DrawerFormLayout
|
|
30
31
|
title={t('Add authenticator')}
|
|
@@ -41,17 +42,19 @@ ctx.viewer.drawer({
|
|
|
41
42
|
|
|
42
43
|
Key props:
|
|
43
44
|
|
|
44
|
-
- `title`: title node
|
|
45
|
-
- `
|
|
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)
|
|
46
47
|
- `submitting`: drives the Submit button's loading state
|
|
47
48
|
- `submitText` / `cancelText`: button labels
|
|
48
49
|
- `footer`: full override of the footer content (replaces the default Cancel + Submit pair)
|
|
49
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
|
+
|
|
50
53
|
#### DialogFormLayout
|
|
51
54
|
|
|
52
55
|
Dialog-style form layout, the centered counterpart of `DrawerFormLayout`. Pair with `ctx.viewer.dialog({ closable: true, content })`.
|
|
53
56
|
|
|
54
|
-
The only visual difference from the drawer version
|
|
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.
|
|
55
58
|
|
|
56
59
|
```tsx
|
|
57
60
|
import { DialogFormLayout } from '@nocobase/client-v2';
|
|
@@ -73,7 +76,7 @@ When to pick which:
|
|
|
73
76
|
- **Drawer**: long forms with lots of fields that benefit from a full-height side panel (settings-page "Add / Edit")
|
|
74
77
|
- **Dialog**: short forms that ask for quick confirmation (bind, change password, two-factor verify)
|
|
75
78
|
|
|
76
|
-
Props are identical to `DrawerFormLayout`
|
|
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.
|
|
77
80
|
|
|
78
81
|
### Form fields
|
|
79
82
|
|
|
@@ -272,6 +275,48 @@ Companion exports:
|
|
|
272
275
|
- `PAGE_SIZE_OPTIONS`: suggested page-size dropdown values `[5, 10, 20, 50, 100, 200]`
|
|
273
276
|
- `SortHandle`: standalone handle component, exported from `@nocobase/client-v2` for embedding into custom columns
|
|
274
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
|
+
|
|
275
320
|
### Utilities
|
|
276
321
|
|
|
277
322
|
#### createFormRegistry
|
|
@@ -302,6 +347,44 @@ Use this when a plugin needs an extension point for "same name + same shape + di
|
|
|
302
347
|
|
|
303
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.
|
|
304
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
|
+
|
|
305
388
|
## When to add a new component here
|
|
306
389
|
|
|
307
390
|
- Two or more plugins need the same field or container shape — promote it to this folder
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
#### DrawerFormLayout
|
|
16
16
|
|
|
17
|
-
抽屉形态的表单 layout。配合 `ctx.viewer.drawer({ content })` 用。
|
|
17
|
+
抽屉形态的表单 layout。配合 `ctx.viewer.drawer({ closable: true, content })` 用。
|
|
18
18
|
|
|
19
|
-
- 顶部 Header
|
|
19
|
+
- 顶部 Header:只放标题;左侧的关闭 X 来自 antd Drawer——必须在 `viewer.drawer` 上显式传 `closable: true` 才会出现
|
|
20
20
|
- 底部 Footer:默认 Cancel / Submit 两个按钮;可以用 `footer` 完全替换
|
|
21
21
|
- 中间 children:调用方自己放 `<Form>` 实例 + 字段
|
|
22
22
|
|
|
@@ -25,6 +25,7 @@ import { DrawerFormLayout } from '@nocobase/client-v2';
|
|
|
25
25
|
|
|
26
26
|
ctx.viewer.drawer({
|
|
27
27
|
width: '50%',
|
|
28
|
+
closable: true, // 关键:开启 antd Drawer 原生关闭 X
|
|
28
29
|
content: () => (
|
|
29
30
|
<DrawerFormLayout
|
|
30
31
|
title={t('添加认证器')}
|
|
@@ -41,17 +42,19 @@ ctx.viewer.drawer({
|
|
|
41
42
|
|
|
42
43
|
主要属性:
|
|
43
44
|
|
|
44
|
-
- `title
|
|
45
|
-
- `
|
|
45
|
+
- `title`:标题节点
|
|
46
|
+
- `onSubmit`:回调,resolve 后会自动关闭抽屉。throw 可以让抽屉保持打开(比如校验失败)
|
|
46
47
|
- `submitting`:驱动 Submit 按钮的 loading
|
|
47
48
|
- `submitText` / `cancelText`:按钮文字
|
|
48
49
|
- `footer`:完全自定义 Footer 内容(覆盖默认两个按钮)
|
|
49
50
|
|
|
51
|
+
需要在关闭前做「未保存改动」之类的确认,用更底层的 `viewer.drawer({ preventClose, beforeClose })`,这层 layout 不再包装 cancel 拦截。
|
|
52
|
+
|
|
50
53
|
#### DialogFormLayout
|
|
51
54
|
|
|
52
|
-
弹窗形态的表单 layout,跟 `DrawerFormLayout`
|
|
55
|
+
弹窗形态的表单 layout,跟 `DrawerFormLayout` 同形。配合 `ctx.viewer.dialog({ closable: true, content })` 用。
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
视觉上的差异只有关闭 X 的位置——Drawer 是 antd Drawer 自带的左上角 X,Dialog 是 antd Modal 自带的右上角 X。两边都依赖在 viewer 调用处显式传 `closable: true`,layout 自己都不渲染 close 图标。
|
|
55
58
|
|
|
56
59
|
```tsx
|
|
57
60
|
import { DialogFormLayout } from '@nocobase/client-v2';
|
|
@@ -73,7 +76,7 @@ ctx.viewer.dialog({
|
|
|
73
76
|
- **Drawer**:长表单、字段多、需要从一侧滑出占用整面(比如设置页的「添加 / 编辑」)
|
|
74
77
|
- **Dialog**:短表单、需要快速确认(比如绑定、修改密码、二次验证)
|
|
75
78
|
|
|
76
|
-
属性跟 `DrawerFormLayout`
|
|
79
|
+
属性跟 `DrawerFormLayout` 基本一致,可以直接换。唯一区别:`DialogFormLayout` 多一个 `onCancel` 回调(Cancel 按钮和原生 X 都会触发),用于「丢弃改动」之类的确认。
|
|
77
80
|
|
|
78
81
|
### 表单字段
|
|
79
82
|
|
|
@@ -270,6 +273,48 @@ import { Table, DEFAULT_PAGE_SIZE } from '@nocobase/client-v2';
|
|
|
270
273
|
- `PAGE_SIZE_OPTIONS`:建议的分页选项 `[5, 10, 20, 50, 100, 200]`
|
|
271
274
|
- `SortHandle`:从 `@nocobase/client-v2` 导出的独立手柄组件,可以嵌进自定义列
|
|
272
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
|
+
|
|
273
318
|
### 工具
|
|
274
319
|
|
|
275
320
|
#### createFormRegistry
|
|
@@ -300,6 +345,43 @@ storageTypes.unregister('local');
|
|
|
300
345
|
|
|
301
346
|
`name` 重复注册会用新条目覆盖旧的,同时打 `console.warn`——HMR 时不抛错,开发期能看到意外的重复。
|
|
302
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
|
+
|
|
303
385
|
## 怎么决定加不加新组件
|
|
304
386
|
|
|
305
387
|
- 出现两个及以上插件需要同一形态的字段或容器——抽到这里
|
|
@@ -16,17 +16,9 @@ export interface DialogFormLayoutProps {
|
|
|
16
16
|
title: React.ReactNode;
|
|
17
17
|
/** Form body — typically a `<Form>` wrapping `<Form.Item>` fields. */
|
|
18
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
|
-
*/
|
|
19
|
+
/** Called before the dialog is closed by the Cancel button or the top-right close (X) icon. Use for "discard changes" confirmations. */
|
|
23
20
|
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
|
-
*/
|
|
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). */
|
|
30
22
|
onSubmit?: () => void | Promise<void>;
|
|
31
23
|
/** Drives the Submit button's loading state. */
|
|
32
24
|
submitting?: boolean;
|
|
@@ -34,30 +26,14 @@ export interface DialogFormLayoutProps {
|
|
|
34
26
|
submitText?: React.ReactNode;
|
|
35
27
|
/** Override the Cancel button label. Defaults to "Cancel". */
|
|
36
28
|
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
|
-
*/
|
|
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). */
|
|
42
30
|
footer?: React.ReactNode;
|
|
43
31
|
}
|
|
44
32
|
|
|
45
33
|
/**
|
|
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.
|
|
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).
|
|
52
35
|
*
|
|
53
|
-
*
|
|
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.
|
|
36
|
+
* Callers own the `<Form>` instance, validation, and the actual API call. This component only handles the chrome and close behaviour.
|
|
61
37
|
*
|
|
62
38
|
* Example:
|
|
63
39
|
*
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { FilterOutlined } from '@ant-design/icons';
|
|
11
|
+
import type { Collection } from '@nocobase/flow-engine';
|
|
12
|
+
import { Button, type ButtonProps, Popover, type PopoverProps } from 'antd';
|
|
13
|
+
import React, { FC, useState } from 'react';
|
|
14
|
+
import { FilterContent } from '../../../flow/components/filter';
|
|
15
|
+
import { CompiledFilter, FilterApplyAction, useFilterActionProps } from './useFilterActionProps';
|
|
16
|
+
|
|
17
|
+
const identity = (s: string) => s;
|
|
18
|
+
|
|
19
|
+
export interface CollectionFilterProps {
|
|
20
|
+
/** Collection whose fields drive the filter row's field picker. */
|
|
21
|
+
collection: Collection | undefined;
|
|
22
|
+
/** Called on Submit or Reset with the compiled NocoBase filter param (`undefined` when cleared). */
|
|
23
|
+
onChange: (filter: CompiledFilter) => void;
|
|
24
|
+
/** Translator. Defaults to identity. */
|
|
25
|
+
t?: (key: string, options?: Record<string, any>) => string;
|
|
26
|
+
/** Whitelist of root-level field names to expose. */
|
|
27
|
+
filterableFieldNames?: string[];
|
|
28
|
+
/** Bypass the `filterableFieldNames` whitelist. */
|
|
29
|
+
noIgnore?: boolean;
|
|
30
|
+
/** Override the trigger button's label. Defaults to `t('Filter')`, or the v1-style `t('{{count}} filter items', { count })` when conditions are present. */
|
|
31
|
+
buttonText?: React.ReactNode;
|
|
32
|
+
/** Swap the default `t('Filter')` label for v1's `t('{{count}} filter items', { count })` when conditions are present. Defaults to `true`. */
|
|
33
|
+
showCount?: boolean;
|
|
34
|
+
/** Pass-through props for the antd `<Popover>`. */
|
|
35
|
+
popoverProps?: Omit<PopoverProps, 'open' | 'onOpenChange' | 'content' | 'children'>;
|
|
36
|
+
/** Pass-through props for the trigger `<Button>`. */
|
|
37
|
+
buttonProps?: Omit<ButtonProps, 'icon' | 'type' | 'children' | 'onClick'>;
|
|
38
|
+
/** Min-width applied to the popover body. Defaults to `520`. */
|
|
39
|
+
popoverMinWidth?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Filter button bound to a collection. Renders an antd `<Popover>` over a `<Button>`; the popover hosts a multi-condition filter form (field picker, operator, value). Submit dismisses the popover and emits the compiled filter via `onChange`; Reset keeps the popover open and emits `undefined`.
|
|
44
|
+
*
|
|
45
|
+
* Pair with `<ExtendCollectionsProvider>` when the target collection is client-only (e.g. a `schema-only` server collection that isn't auto-published to the v2 data source).
|
|
46
|
+
*/
|
|
47
|
+
export const CollectionFilter: FC<CollectionFilterProps> = (props) => {
|
|
48
|
+
const {
|
|
49
|
+
collection,
|
|
50
|
+
onChange,
|
|
51
|
+
t = identity,
|
|
52
|
+
filterableFieldNames,
|
|
53
|
+
noIgnore,
|
|
54
|
+
buttonText,
|
|
55
|
+
showCount = true,
|
|
56
|
+
popoverProps,
|
|
57
|
+
buttonProps,
|
|
58
|
+
popoverMinWidth = 520,
|
|
59
|
+
} = props;
|
|
60
|
+
|
|
61
|
+
const [open, setOpen] = useState(false);
|
|
62
|
+
|
|
63
|
+
const filterAction = useFilterActionProps({
|
|
64
|
+
collection,
|
|
65
|
+
filterableFieldNames,
|
|
66
|
+
noIgnore,
|
|
67
|
+
t,
|
|
68
|
+
onApply: (filter: CompiledFilter, action: FilterApplyAction) => {
|
|
69
|
+
onChange(filter);
|
|
70
|
+
if (action === 'submit') setOpen(false);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Matches v1's `Filter.Action`: when at least one condition is set, the button label switches to the count-aware string (`"N 个筛选项"` in zh-CN). The button itself stays in the default (white) style — v1 never flipped it to `type='primary'`.
|
|
75
|
+
const label =
|
|
76
|
+
buttonText ??
|
|
77
|
+
(showCount && filterAction.conditionCount > 0
|
|
78
|
+
? t('{{count}} filter items', { count: filterAction.conditionCount })
|
|
79
|
+
: t('Filter'));
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Popover
|
|
83
|
+
trigger="click"
|
|
84
|
+
placement="bottomLeft"
|
|
85
|
+
{...popoverProps}
|
|
86
|
+
open={open}
|
|
87
|
+
onOpenChange={setOpen}
|
|
88
|
+
content={
|
|
89
|
+
<div style={{ minWidth: popoverMinWidth }}>
|
|
90
|
+
<FilterContent value={filterAction.value} ctx={filterAction.ctx} FilterItem={filterAction.FilterItem} />
|
|
91
|
+
</div>
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
<Button icon={<FilterOutlined />} disabled={!collection} {...buttonProps}>
|
|
95
|
+
{label}
|
|
96
|
+
</Button>
|
|
97
|
+
</Popover>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default CollectionFilter;
|