@nocobase/client-v2 2.1.0-alpha.38 → 2.1.0-alpha.39

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 (71) hide show
  1. package/es/APIClient.d.ts +16 -0
  2. package/es/Application.d.ts +2 -1
  3. package/es/BaseApplication.d.ts +6 -0
  4. package/es/PluginManager.d.ts +2 -0
  5. package/es/authRedirect.d.ts +9 -16
  6. package/es/components/form/EnvVariableInput.d.ts +8 -6
  7. package/es/components/form/VariableInput.d.ts +73 -0
  8. package/es/components/form/index.d.ts +1 -0
  9. package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
  10. package/es/components/form/table/SelectionCell.d.ts +36 -0
  11. package/es/components/form/table/Table.d.ts +82 -0
  12. package/es/components/form/table/constants.d.ts +15 -0
  13. package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
  14. package/es/components/form/table/dnd/index.d.ts +9 -0
  15. package/es/components/form/table/index.d.ts +9 -0
  16. package/es/components/form/table/styles.d.ts +41 -0
  17. package/es/components/form/table/utils.d.ts +44 -0
  18. package/es/components/index.d.ts +2 -0
  19. package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +9 -1
  21. package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
  22. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
  23. package/es/flow-compat/passwordUtils.d.ts +1 -1
  24. package/es/index.d.ts +1 -0
  25. package/es/index.mjs +164 -97
  26. package/es/json-logic/globalOperators.d.ts +11 -0
  27. package/es/theme/globalStyles.d.ts +9 -0
  28. package/es/theme/index.d.ts +1 -0
  29. package/es/utils/globalDeps.d.ts +7 -0
  30. package/lib/index.js +173 -106
  31. package/package.json +9 -6
  32. package/src/APIClient.ts +68 -0
  33. package/src/Application.tsx +6 -2
  34. package/src/BaseApplication.tsx +8 -0
  35. package/src/PluginManager.ts +2 -0
  36. package/src/__tests__/app.test.tsx +8 -0
  37. package/src/__tests__/authRedirect.test.ts +170 -64
  38. package/src/__tests__/globalDeps.test.ts +2 -0
  39. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
  40. package/src/__tests__/remotePlugins.test.ts +148 -0
  41. package/src/authRedirect.ts +23 -84
  42. package/src/components/form/EnvVariableInput.tsx +11 -46
  43. package/src/components/form/VariableInput.tsx +177 -0
  44. package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
  45. package/src/components/form/index.tsx +1 -0
  46. package/src/components/form/table/RowOverlayPreview.tsx +51 -0
  47. package/src/components/form/table/SelectionCell.tsx +72 -0
  48. package/src/components/form/table/Table.tsx +279 -0
  49. package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
  50. package/src/components/form/table/constants.ts +16 -0
  51. package/src/components/form/table/dnd/SortableRow.tsx +106 -0
  52. package/src/components/form/table/dnd/index.ts +10 -0
  53. package/src/components/form/table/index.tsx +13 -0
  54. package/src/components/form/table/styles.ts +110 -0
  55. package/src/components/form/table/utils.ts +75 -0
  56. package/src/components/index.ts +2 -0
  57. package/src/css-variable/CSSVariableProvider.tsx +1 -1
  58. package/src/flow/actions/filterFormDefaultValues.tsx +1 -2
  59. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
  61. package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
  62. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +329 -5
  63. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +337 -0
  64. package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
  65. package/src/index.ts +1 -0
  66. package/src/json-logic/globalOperators.js +731 -0
  67. package/src/nocobase-buildin-plugin/index.tsx +4 -4
  68. package/src/theme/globalStyles.ts +21 -0
  69. package/src/theme/index.tsx +1 -0
  70. package/src/utils/globalDeps.ts +50 -30
  71. package/src/utils/remotePlugins.ts +107 -6
@@ -7,10 +7,97 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
+ import React from 'react';
11
+ import { render, waitFor } from '@testing-library/react';
12
+ import { FlowEngine } from '@nocobase/flow-engine';
10
13
  import { describe, expect, it, vi } from 'vitest';
11
14
  import { filterFormDefaultValues } from '../../../../actions/filterFormDefaultValues';
12
15
  import { FilterFormBlockModel } from '../FilterFormBlockModel';
13
16
 
17
+ function resolveTemplateValue(raw: any, values: Record<string, any>): any {
18
+ if (typeof raw === 'string') {
19
+ const matched = raw.match(/^\{\{\s*ctx\.formValues\.([^}]+?)\s*\}\}$/);
20
+ return matched ? values[matched[1]] : raw;
21
+ }
22
+ if (Array.isArray(raw)) {
23
+ return raw.map((item) => resolveTemplateValue(item, values));
24
+ }
25
+ if (raw && typeof raw === 'object') {
26
+ return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, resolveTemplateValue(value, values)]));
27
+ }
28
+ return raw;
29
+ }
30
+
31
+ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<string, any> = {}) {
32
+ const values = { ...initialValues };
33
+ const createItem = (fieldPath: string, uid: string) => ({
34
+ uid,
35
+ fieldPath,
36
+ props: { name: `${fieldPath}_${uid}` },
37
+ getProps() {
38
+ return this.props;
39
+ },
40
+ getStepParams(flowKey: string, stepKey: string) {
41
+ if (flowKey === 'fieldSettings' && stepKey === 'init') {
42
+ return { fieldPath };
43
+ }
44
+ return undefined;
45
+ },
46
+ subModels: {
47
+ field: {},
48
+ },
49
+ });
50
+ const model = {
51
+ defaultValuesRefreshSeq: 0,
52
+ lastDefaultValueByFieldName: new Map<string, any>(),
53
+ form: {
54
+ getFieldsValue: () => ({ ...values }),
55
+ getFieldValue: (name: string) => values[name],
56
+ setFieldValue: (name: string, value: any) => {
57
+ values[name] = value;
58
+ },
59
+ setFieldsValue: (next: Record<string, any>) => {
60
+ Object.assign(values, next);
61
+ },
62
+ },
63
+ context: {
64
+ resolveJsonTemplate: vi.fn((raw) => resolveTemplateValue(raw, values)),
65
+ app: {
66
+ jsonLogic: {
67
+ apply: vi.fn((logic: Record<string, any[]>) => {
68
+ const [[operator, args]] = Object.entries(logic);
69
+ if (operator === '$eq') return args[0] === args[1];
70
+ return true;
71
+ }),
72
+ },
73
+ },
74
+ },
75
+ subModels: {
76
+ grid: {
77
+ subModels: {
78
+ items: [createItem('nickname', 'nick'), createItem('username', 'user')],
79
+ },
80
+ },
81
+ },
82
+ getStepParams: vi.fn((flowKey: string, stepKey: string) => {
83
+ if (flowKey === 'formFilterBlockModelSettings' && stepKey === 'defaultValues') {
84
+ return { value: rules };
85
+ }
86
+ return undefined;
87
+ }),
88
+ canApplyFormDefaultValue: (FilterFormBlockModel.prototype as any).canApplyFormDefaultValue,
89
+ matchDefaultValueCondition: (FilterFormBlockModel.prototype as any).matchDefaultValueCondition,
90
+ applyFormDefaultValues: FilterFormBlockModel.prototype.applyFormDefaultValues,
91
+ handleFilterFormValuesChange: (FilterFormBlockModel.prototype as any).handleFilterFormValuesChange,
92
+ dispatchEvent: vi.fn(),
93
+ emitter: {
94
+ emit: vi.fn(),
95
+ },
96
+ };
97
+
98
+ return { model, values };
99
+ }
100
+
14
101
  describe('filter-form defaultValues wiring', () => {
15
102
  it('loads action and model modules', () => {
16
103
  expect(filterFormDefaultValues).toBeTruthy();
@@ -48,4 +135,254 @@ describe('filter-form defaultValues wiring', () => {
48
135
  expect(model.initialDefaultsPromise).toBeUndefined();
49
136
  expect(model.applyFormDefaultValues).not.toHaveBeenCalled();
50
137
  });
138
+
139
+ it('exposes current form values in the filter form variable meta tree', async () => {
140
+ const engine = new FlowEngine();
141
+
142
+ const dataSource = engine.context.dataSourceManager.getDataSource('main');
143
+ dataSource.addCollection({
144
+ name: 'users',
145
+ filterTargetKey: ['id', 'tenantId'],
146
+ fields: [
147
+ { name: 'id', type: 'integer', interface: 'number' },
148
+ { name: 'tenantId', type: 'string', interface: 'text' },
149
+ { name: 'name', type: 'string', interface: 'text' },
150
+ ],
151
+ });
152
+ dataSource.addCollection({
153
+ name: 'departments',
154
+ filterTargetKey: 'id',
155
+ fields: [
156
+ { name: 'id', type: 'integer', interface: 'number' },
157
+ { name: 'name', type: 'string', interface: 'text' },
158
+ { name: 'owner', type: 'belongsTo', target: 'users', interface: 'm2o' },
159
+ ],
160
+ });
161
+ dataSource.addCollection({
162
+ name: 'tasks',
163
+ filterTargetKey: 'id',
164
+ fields: [
165
+ { name: 'id', type: 'integer', interface: 'number' },
166
+ { name: 'title', type: 'string', interface: 'text' },
167
+ { name: 'department', type: 'belongsTo', target: 'departments', interface: 'm2o' },
168
+ ],
169
+ });
170
+
171
+ engine.registerModels({ FilterFormBlockModel });
172
+ const model = engine.createModel<FilterFormBlockModel>({
173
+ use: 'FilterFormBlockModel',
174
+ uid: 'filter-form-current-form',
175
+ subModels: {
176
+ grid: {
177
+ subModels: {
178
+ items: [],
179
+ },
180
+ },
181
+ },
182
+ } as any);
183
+
184
+ function HookCaller() {
185
+ model.useHooksBeforeRender();
186
+ return null;
187
+ }
188
+
189
+ render(React.createElement(HookCaller));
190
+
191
+ const store = {
192
+ title: 'bug',
193
+ 'department_department-filter': 1,
194
+ 'department.owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
195
+ };
196
+ const fakeForm = {
197
+ getFieldsValue: () => ({ ...store }),
198
+ };
199
+ model.context.defineProperty('form', { value: fakeForm });
200
+ model.subModels.grid.subModels.items = [
201
+ {
202
+ uid: 'department-filter',
203
+ fieldPath: 'department',
204
+ props: { name: 'department_department-filter' },
205
+ subModels: {
206
+ field: {
207
+ context: {
208
+ collectionField: dataSource.getCollection('tasks').getField('department'),
209
+ },
210
+ },
211
+ },
212
+ },
213
+ {
214
+ uid: 'owner-filter',
215
+ fieldPath: 'department.owner',
216
+ props: { name: 'department.owner_owner-filter' },
217
+ subModels: {
218
+ field: {
219
+ context: {
220
+ collectionField: dataSource.getCollection('departments').getField('owner'),
221
+ },
222
+ },
223
+ },
224
+ },
225
+ ];
226
+
227
+ expect((model.context as any).formValues).toMatchObject({
228
+ ...store,
229
+ department: {
230
+ 'owner_owner-filter': { id: 7, tenantId: 'tenant-a' },
231
+ },
232
+ });
233
+
234
+ const options = (model.context as any).getPropertyOptions('formValues');
235
+ const meta = await options.meta();
236
+ const properties = await meta.properties();
237
+ const metaTree = await (model.context as any).getPropertyMetaTree();
238
+
239
+ expect(options.resolveOnServer('department_department-filter')).toBe(false);
240
+ expect(options.resolveOnServer('department_department-filter.name')).toBe(true);
241
+ expect(options.resolveOnServer('department_department-filter[0].name')).toBe(true);
242
+ expect(options.resolveOnServer('department.owner_owner-filter.name')).toBe(true);
243
+ expect(options.serverOnlyWhenContextParams).toBe(true);
244
+ expect(meta.title).toBe('Current form');
245
+ expect(properties.department.properties['owner_owner-filter'].title).toBe('owner');
246
+ expect(metaTree).toEqual(
247
+ expect.arrayContaining([
248
+ expect.objectContaining({
249
+ name: 'formValues',
250
+ title: 'Current form',
251
+ }),
252
+ ]),
253
+ );
254
+ expect(await meta.buildVariablesParams(model.context)).toMatchObject({
255
+ 'department_department-filter': { collection: 'departments', dataSourceKey: 'main', filterByTk: 1 },
256
+ department: {
257
+ 'owner_owner-filter': {
258
+ collection: 'users',
259
+ dataSourceKey: 'main',
260
+ filterByTk: { id: 7, tenantId: 'tenant-a' },
261
+ },
262
+ },
263
+ });
264
+ });
265
+
266
+ it('refreshes default values that depend on current filter form values', async () => {
267
+ const { model, values } = createFilterFormDefaultValuesModel(
268
+ [
269
+ {
270
+ key: 'username-default',
271
+ enable: true,
272
+ targetPath: 'username',
273
+ mode: 'default',
274
+ value: '{{ ctx.formValues.nickname_nick }}',
275
+ },
276
+ ],
277
+ { nickname_nick: 'Alice' },
278
+ );
279
+
280
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
281
+ expect(values.username_user).toBe('Alice');
282
+
283
+ values.nickname_nick = 'Bob';
284
+ model.defaultValuesRefreshSeq += 1;
285
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
286
+ refreshSeq: model.defaultValuesRefreshSeq,
287
+ });
288
+ expect(values.username_user).toBe('Bob');
289
+
290
+ values.username_user = 'Manual';
291
+ values.nickname_nick = 'Carol';
292
+ model.defaultValuesRefreshSeq += 1;
293
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any, {
294
+ refreshSeq: model.defaultValuesRefreshSeq,
295
+ });
296
+ expect(values.username_user).toBe('Manual');
297
+ });
298
+
299
+ it('applies fixed values even when the target filter field already has a value', async () => {
300
+ const { model, values } = createFilterFormDefaultValuesModel(
301
+ [
302
+ {
303
+ key: 'username-fixed',
304
+ enable: true,
305
+ targetPath: 'username',
306
+ mode: 'assign',
307
+ value: '{{ ctx.formValues.nickname_nick }}',
308
+ },
309
+ ],
310
+ { nickname_nick: 'Bob', username_user: 'Manual' },
311
+ );
312
+
313
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
314
+
315
+ expect(values.username_user).toBe('Bob');
316
+ });
317
+
318
+ it('skips filter form field values when the rule condition does not match', async () => {
319
+ const { model, values } = createFilterFormDefaultValuesModel(
320
+ [
321
+ {
322
+ key: 'username-condition',
323
+ enable: true,
324
+ targetPath: 'username',
325
+ mode: 'assign',
326
+ condition: {
327
+ logic: '$and',
328
+ items: [{ path: '{{ ctx.formValues.nickname_nick }}', operator: '$eq', value: 'allow' }],
329
+ },
330
+ value: 'Matched',
331
+ },
332
+ ],
333
+ { nickname_nick: 'deny' },
334
+ );
335
+
336
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
337
+ expect(values.username_user).toBeUndefined();
338
+
339
+ values.nickname_nick = 'allow';
340
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
341
+ expect(values.username_user).toBe('Matched');
342
+ });
343
+
344
+ it('emits formValuesChange with final values after applying dependent field values', async () => {
345
+ const { model, values } = createFilterFormDefaultValuesModel(
346
+ [
347
+ {
348
+ key: 'username-fixed',
349
+ enable: true,
350
+ targetPath: 'username',
351
+ mode: 'assign',
352
+ value: '{{ ctx.formValues.nickname_nick }}',
353
+ },
354
+ ],
355
+ { nickname_nick: 'Bob' },
356
+ );
357
+
358
+ (model as any).handleFilterFormValuesChange({ nickname_nick: 'Bob' }, { nickname_nick: 'Bob' });
359
+
360
+ await waitFor(() => {
361
+ expect(values.username_user).toBe('Bob');
362
+ expect(model.dispatchEvent).toHaveBeenCalledWith(
363
+ 'formValuesChange',
364
+ {
365
+ changedValues: {
366
+ nickname_nick: 'Bob',
367
+ username_user: 'Bob',
368
+ },
369
+ allValues: {
370
+ nickname_nick: 'Bob',
371
+ username_user: 'Bob',
372
+ },
373
+ },
374
+ { debounce: true },
375
+ );
376
+ });
377
+ expect(model.emitter.emit).toHaveBeenCalledWith('formValuesChange', {
378
+ changedValues: {
379
+ nickname_nick: 'Bob',
380
+ username_user: 'Bob',
381
+ },
382
+ allValues: {
383
+ nickname_nick: 'Bob',
384
+ username_user: 'Bob',
385
+ },
386
+ });
387
+ });
51
388
  });
@@ -7,84 +7,4 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { MenuOutlined } from '@ant-design/icons';
11
- import { TinyColor } from '@ctrl/tinycolor';
12
- import { useSortable } from '@dnd-kit/sortable';
13
- import { css } from '@emotion/css';
14
- import { theme } from 'antd';
15
- import classNames from 'classnames';
16
- import React, { useMemo } from 'react';
17
-
18
- type DragSortRowContextValue = {
19
- attributes?: Record<string, unknown>;
20
- listeners?: Record<string, unknown>;
21
- setActivatorNodeRef?: (node: HTMLElement | null) => void;
22
- };
23
-
24
- const DragSortRowContext = React.createContext<DragSortRowContextValue | null>(null);
25
-
26
- const sortHandleClass = css`
27
- display: inline-flex;
28
- align-items: center;
29
- justify-content: center;
30
- cursor: grab;
31
- `;
32
-
33
- export const SortHandle: React.FC<{ id: string | number; style?: React.CSSProperties }> = (props) => {
34
- const { id: _id, ...otherProps } = props;
35
- const dragSortContext = React.useContext(DragSortRowContext);
36
- // return <MenuOutlined ref={setNodeRef} {...otherProps} {...listeners} style={{ cursor: 'grab' }} />;
37
- return (
38
- <span
39
- ref={dragSortContext?.setActivatorNodeRef}
40
- {...dragSortContext?.attributes}
41
- {...dragSortContext?.listeners}
42
- {...otherProps}
43
- className={classNames(sortHandleClass)}
44
- >
45
- <MenuOutlined />
46
- </span>
47
- );
48
- };
49
-
50
- export const SortableRow = (props) => {
51
- const { token }: any = theme.useToken();
52
- const id = props['data-row-key']?.toString();
53
- const { setNodeRef, setActivatorNodeRef, attributes, listeners, active, over } = useSortable({
54
- id,
55
- });
56
- const { rowIndex, ...others } = props;
57
- const isOver = over?.id === id;
58
- const classObj = useMemo(() => {
59
- const borderColor = new TinyColor(token.colorPrimary).setAlpha(0.6).toHex8String();
60
- return {
61
- topActiveClass: css`
62
- & > td {
63
- border-top: 2px solid ${borderColor} !important;
64
- }
65
- `,
66
- bottomActiveClass: css`
67
- & > td {
68
- border-bottom: 2px solid ${borderColor} !important;
69
- }
70
- `,
71
- };
72
- }, [token.colorPrimary]);
73
-
74
- const className =
75
- (active?.data.current?.sortable.index ?? -1) > rowIndex ? classObj.topActiveClass : classObj.bottomActiveClass;
76
-
77
- const row = (
78
- <DragSortRowContext.Provider value={{ listeners, setActivatorNodeRef }}>
79
- <tr
80
- ref={(node) => {
81
- setNodeRef(node);
82
- }}
83
- {...others}
84
- className={classNames(props.className, { [className]: active && isOver })}
85
- />
86
- </DragSortRowContext.Provider>
87
- );
88
-
89
- return row;
90
- };
10
+ export { DragSortRowContext, SortHandle, SortableRow } from '../../../../../components/form/table/dnd/SortableRow';
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import 'antd/dist/reset.css';
11
11
 
12
+ export * from './APIClient';
12
13
  export * from './BaseApplication';
13
14
  export * from './Application';
14
15
  export * from './PinnedPluginListContext';