@nocobase/client-v2 2.1.0-beta.33 → 2.1.0-beta.34

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 (61) hide show
  1. package/es/APIClient.d.ts +16 -0
  2. package/es/Application.d.ts +2 -1
  3. package/es/authRedirect.d.ts +9 -16
  4. package/es/components/form/EnvVariableInput.d.ts +8 -6
  5. package/es/components/form/VariableInput.d.ts +73 -0
  6. package/es/components/form/index.d.ts +1 -0
  7. package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
  8. package/es/components/form/table/SelectionCell.d.ts +36 -0
  9. package/es/components/form/table/Table.d.ts +82 -0
  10. package/es/components/form/table/constants.d.ts +15 -0
  11. package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
  12. package/es/components/form/table/dnd/index.d.ts +9 -0
  13. package/es/components/form/table/index.d.ts +9 -0
  14. package/es/components/form/table/styles.d.ts +41 -0
  15. package/es/components/form/table/utils.d.ts +44 -0
  16. package/es/components/index.d.ts +2 -0
  17. package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
  18. package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
  19. package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
  20. package/es/flow-compat/passwordUtils.d.ts +1 -1
  21. package/es/index.d.ts +1 -0
  22. package/es/index.mjs +145 -78
  23. package/es/theme/globalStyles.d.ts +9 -0
  24. package/es/theme/index.d.ts +1 -0
  25. package/lib/index.js +161 -94
  26. package/package.json +8 -6
  27. package/src/APIClient.ts +68 -0
  28. package/src/Application.tsx +6 -2
  29. package/src/__tests__/authRedirect.test.ts +170 -64
  30. package/src/__tests__/globalDeps.test.ts +2 -0
  31. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
  32. package/src/authRedirect.ts +23 -84
  33. package/src/components/form/EnvVariableInput.tsx +11 -46
  34. package/src/components/form/VariableInput.tsx +177 -0
  35. package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
  36. package/src/components/form/index.tsx +1 -0
  37. package/src/components/form/table/RowOverlayPreview.tsx +51 -0
  38. package/src/components/form/table/SelectionCell.tsx +72 -0
  39. package/src/components/form/table/Table.tsx +279 -0
  40. package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
  41. package/src/components/form/table/constants.ts +16 -0
  42. package/src/components/form/table/dnd/SortableRow.tsx +106 -0
  43. package/src/components/form/table/dnd/index.ts +10 -0
  44. package/src/components/form/table/index.tsx +13 -0
  45. package/src/components/form/table/styles.ts +110 -0
  46. package/src/components/form/table/utils.ts +75 -0
  47. package/src/components/index.ts +2 -0
  48. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
  49. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
  50. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
  51. package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
  52. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
  53. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
  54. package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
  55. package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
  56. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
  57. package/src/index.ts +1 -0
  58. package/src/nocobase-buildin-plugin/index.tsx +4 -4
  59. package/src/theme/globalStyles.ts +21 -0
  60. package/src/theme/index.tsx +1 -0
  61. package/src/utils/globalDeps.ts +5 -1
@@ -69,6 +69,106 @@ function resolveScriptCode(codeParam?: string) {
69
69
  return typeof raw === 'string' ? raw.trim() : '';
70
70
  }
71
71
 
72
+ type NamePathPart = string | number;
73
+
74
+ function toNamePath(input: unknown): NamePathPart[] | null {
75
+ if (Array.isArray(input)) {
76
+ return input.filter((item): item is NamePathPart => typeof item === 'string' || typeof item === 'number');
77
+ }
78
+ if (typeof input === 'number') {
79
+ return [input];
80
+ }
81
+ if (typeof input === 'string') {
82
+ return input
83
+ .split('.')
84
+ .map((item) => item.trim())
85
+ .filter(Boolean);
86
+ }
87
+ return null;
88
+ }
89
+
90
+ function startsWithNamePath(namePath: NamePathPart[], prefix: NamePathPart[]) {
91
+ return prefix.length <= namePath.length && prefix.every((item, index) => String(namePath[index]) === String(item));
92
+ }
93
+
94
+ function getFieldSettingsNamePath(model: any): NamePathPart[] | null {
95
+ const init =
96
+ model?.getStepParams?.('fieldSettings', 'init') || model?.parent?.getStepParams?.('fieldSettings', 'init');
97
+ const fieldPath = toNamePath(init?.fieldPath);
98
+ const associationPath = toNamePath(init?.associationPathName);
99
+
100
+ if (!fieldPath?.length) {
101
+ return null;
102
+ }
103
+
104
+ if (!associationPath?.length || startsWithNamePath(fieldPath, associationPath)) {
105
+ return fieldPath;
106
+ }
107
+
108
+ return [...associationPath, ...fieldPath];
109
+ }
110
+
111
+ function applyFieldIndex(namePath: NamePathPart[] | null, fieldIndex: unknown): NamePathPart[] | null {
112
+ if (!namePath?.length) {
113
+ return null;
114
+ }
115
+ if (namePath.some((item) => typeof item === 'number') || !Array.isArray(fieldIndex) || fieldIndex.length === 0) {
116
+ return namePath;
117
+ }
118
+
119
+ const indexQueues = new Map<string, number[]>();
120
+ for (const item of fieldIndex) {
121
+ if (typeof item !== 'string') continue;
122
+ const [fieldName, indexStr] = item.split(':');
123
+ const index = Number(indexStr);
124
+ if (!fieldName || !Number.isFinite(index)) continue;
125
+ const queue = indexQueues.get(fieldName) || [];
126
+ queue.push(index);
127
+ indexQueues.set(fieldName, queue);
128
+ }
129
+
130
+ if (!indexQueues.size) {
131
+ return namePath;
132
+ }
133
+
134
+ const result: NamePathPart[] = [];
135
+ for (const item of namePath) {
136
+ result.push(item);
137
+ const queue = indexQueues.get(String(item));
138
+ if (queue?.length) {
139
+ result.push(queue.shift() as number);
140
+ }
141
+ }
142
+ return result;
143
+ }
144
+
145
+ function resolveEffectiveNamePath(ctx: any): NamePathPart[] | null {
146
+ const namePath =
147
+ getFieldSettingsNamePath(ctx.model) || toNamePath(ctx.fieldPathArray) || toNamePath(ctx.model?.props?.name);
148
+ return applyFieldIndex(namePath, ctx.fieldIndex);
149
+ }
150
+
151
+ function setFormValue(form: any, namePath: NamePathPart[], value: any) {
152
+ if (typeof form?.setFieldValue === 'function') {
153
+ form.setFieldValue(namePath, value);
154
+ return;
155
+ }
156
+
157
+ if (typeof form?.setFieldsValue === 'function') {
158
+ const patch: any = {};
159
+ let cursor = patch;
160
+ namePath.forEach((item, index) => {
161
+ if (index === namePath.length - 1) {
162
+ cursor[item] = value;
163
+ return;
164
+ }
165
+ cursor[item] = typeof namePath[index + 1] === 'number' ? [] : {};
166
+ cursor = cursor[item];
167
+ });
168
+ form.setFieldsValue(patch);
169
+ }
170
+ }
171
+
72
172
  const JSFormRuntime: React.FC<{
73
173
  model: JSEditableFieldModel;
74
174
  value?: any;
@@ -274,9 +374,9 @@ JSEditableFieldModel.registerFlow({
274
374
  cache: false,
275
375
  });
276
376
  ctx.defineMethod('getValue', () => {
277
- const name = ctx.model.props?.name;
278
- if (name !== undefined && name !== null) {
279
- const fv = ctx.form?.getFieldValue?.(name);
377
+ const namePath = resolveEffectiveNamePath(ctx);
378
+ if (namePath?.length) {
379
+ const fv = ctx.form?.getFieldValue?.(namePath);
280
380
  return fv !== undefined ? fv : ctx.model.props?.value;
281
381
  }
282
382
  return ctx.model.props?.value;
@@ -284,15 +384,15 @@ JSEditableFieldModel.registerFlow({
284
384
  ctx.defineMethod('setValue', (v) => {
285
385
  try {
286
386
  ctx.model.setProps('value', v);
287
- const name = ctx.model.props?.name;
288
- if (name !== undefined && name !== null) {
289
- ctx.form?.setFieldValue?.(name, v);
387
+ const namePath = resolveEffectiveNamePath(ctx);
388
+ if (namePath?.length) {
389
+ setFormValue(ctx.form, namePath, v);
290
390
  }
291
391
  } catch (_) {
292
392
  // ignore
293
393
  }
294
394
  });
295
- ctx.defineProperty('namePath', { get: () => ctx.model.props?.name, cache: false });
395
+ ctx.defineProperty('namePath', { get: () => resolveEffectiveNamePath(ctx), cache: false });
296
396
  ctx.defineProperty('disabled', { get: () => !!ctx.model.props?.disabled, cache: false });
297
397
  ctx.defineProperty('readOnly', {
298
398
  get: () => isReadOnlyMode(ctx.model),
@@ -11,6 +11,7 @@ import React from 'react';
11
11
  import { act, render, screen, waitFor } from '@testing-library/react';
12
12
  import { describe, expect, it, vi } from 'vitest';
13
13
  import { FlowEngine, FlowEngineProvider, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
14
+ import { get as lodashGet, set as lodashSet } from 'lodash';
14
15
  import { JSEditableFieldModel } from '../JSEditableFieldModel';
15
16
 
16
17
  function createField(props?: Record<string, any>, code = '') {
@@ -73,6 +74,21 @@ const React = ctx.React;
73
74
  ctx.render(<span data-testid="js-readonly-state">{String(ctx.readOnly)}</span>);
74
75
  `;
75
76
 
77
+ const SET_VALUE_AND_RENDER_NAME_PATH_CODE = `
78
+ const React = ctx.React;
79
+ ctx.setValue?.('44');
80
+ ctx.render(<span data-testid="js-name-path">{JSON.stringify(ctx.namePath)}</span>);
81
+ `;
82
+
83
+ function createFormStub(initialValues: any = {}) {
84
+ const store = JSON.parse(JSON.stringify(initialValues));
85
+ return {
86
+ getFieldValue: (namePath: any) => lodashGet(store, namePath),
87
+ setFieldValue: (namePath: any, value: any) => lodashSet(store, namePath, value),
88
+ getFieldsValue: () => store,
89
+ };
90
+ }
91
+
76
92
  class ParentModel extends FlowModel<any> {
77
93
  render() {
78
94
  return <FlowModelRenderer model={this.subModels.field} />;
@@ -83,6 +99,11 @@ function renderParentFieldWithFlowRenderer(
83
99
  fieldProps?: Record<string, any>,
84
100
  parentProps?: Record<string, any>,
85
101
  code = EDITABLE_CODE,
102
+ options?: {
103
+ fieldIndex?: string[];
104
+ fieldStepParams?: Record<string, any>;
105
+ form?: any;
106
+ },
86
107
  ) {
87
108
  const engine = new FlowEngine();
88
109
  engine.registerModels({ JSEditableFieldModel, ParentModel });
@@ -96,6 +117,7 @@ function renderParentFieldWithFlowRenderer(
96
117
  uid: 'js-field-with-parent',
97
118
  props: fieldProps,
98
119
  stepParams: {
120
+ ...(options?.fieldStepParams || {}),
99
121
  jsSettings: {
100
122
  runJs: {
101
123
  code,
@@ -106,6 +128,13 @@ function renderParentFieldWithFlowRenderer(
106
128
  },
107
129
  });
108
130
 
131
+ if (options?.form) {
132
+ parent.context.defineProperty('form', { value: options.form });
133
+ }
134
+ if (options?.fieldIndex) {
135
+ parent.subModels.field.context.defineProperty('fieldIndex', { value: options.fieldIndex });
136
+ }
137
+
109
138
  render(
110
139
  <FlowEngineProvider engine={engine}>
111
140
  <FlowModelRenderer model={parent} />
@@ -207,4 +236,72 @@ describe('JSEditableFieldModel', () => {
207
236
  applyFlowSpy.mockRestore();
208
237
  }
209
238
  });
239
+
240
+ it('writes top-level form values through the effective name path', async () => {
241
+ const form = createFormStub({});
242
+
243
+ renderParentFieldWithFlowRenderer({ name: 'staffname' }, undefined, SET_VALUE_AND_RENDER_NAME_PATH_CODE, {
244
+ form,
245
+ fieldStepParams: {
246
+ fieldSettings: {
247
+ init: {
248
+ fieldPath: 'staffname',
249
+ },
250
+ },
251
+ },
252
+ });
253
+
254
+ await waitFor(() => {
255
+ expect(form.getFieldValue(['staffname'])).toBe('44');
256
+ expect(screen.getByTestId('js-name-path')).toHaveTextContent(JSON.stringify(['staffname']));
257
+ });
258
+ });
259
+
260
+ it('writes subform list values under the association path instead of the form root', async () => {
261
+ const form = createFormStub({ org_o2m: [{}] });
262
+
263
+ renderParentFieldWithFlowRenderer({ name: 'orgname' }, undefined, SET_VALUE_AND_RENDER_NAME_PATH_CODE, {
264
+ form,
265
+ fieldIndex: ['org_o2m:0'],
266
+ fieldStepParams: {
267
+ fieldSettings: {
268
+ init: {
269
+ fieldPath: 'orgname',
270
+ associationPathName: 'org_o2m',
271
+ },
272
+ },
273
+ },
274
+ });
275
+
276
+ await waitFor(() => {
277
+ expect(form.getFieldValue(['org_o2m', 0, 'orgname'])).toBe('44');
278
+ expect(form.getFieldValue(['orgname'])).toBeUndefined();
279
+ expect(screen.getByTestId('js-name-path')).toHaveTextContent(JSON.stringify(['org_o2m', 0, 'orgname']));
280
+ });
281
+ });
282
+
283
+ it('writes nested subform list values with the full field index chain', async () => {
284
+ const form = createFormStub({ users: [{ roles: [{}, {}] }] });
285
+
286
+ renderParentFieldWithFlowRenderer({ name: 'roleName' }, undefined, SET_VALUE_AND_RENDER_NAME_PATH_CODE, {
287
+ form,
288
+ fieldIndex: ['users:0', 'roles:1'],
289
+ fieldStepParams: {
290
+ fieldSettings: {
291
+ init: {
292
+ fieldPath: 'roleName',
293
+ associationPathName: 'users.roles',
294
+ },
295
+ },
296
+ },
297
+ });
298
+
299
+ await waitFor(() => {
300
+ expect(form.getFieldValue(['users', 0, 'roles', 1, 'roleName'])).toBe('44');
301
+ expect(form.getFieldValue(['roleName'])).toBeUndefined();
302
+ expect(screen.getByTestId('js-name-path')).toHaveTextContent(
303
+ JSON.stringify(['users', 0, 'roles', 1, 'roleName']),
304
+ );
305
+ });
306
+ });
210
307
  });
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';
@@ -11,7 +11,7 @@ import { createCollectionContextMeta } from '@nocobase/flow-engine';
11
11
  import React, { createContext, type FC, useEffect, useRef, useState } from 'react';
12
12
  import { Navigate, Outlet, useLocation } from 'react-router-dom';
13
13
  import type { Application } from '../Application';
14
- import { getCurrentV2RedirectPath, getDefaultV2AdminRedirectPath, redirectToLegacySignin } from '../authRedirect';
14
+ import { getCurrentV2RedirectPath, getDefaultV2AdminRedirectPath, redirectToV2Signin } from '../authRedirect';
15
15
  import { AppNotFound } from '../components';
16
16
  import { PluginFlowEngine } from '../flow';
17
17
  import { AdminLayoutMenuItemModel, AdminLayoutModel } from '../flow/admin-shell/admin-layout';
@@ -144,7 +144,7 @@ const CurrentUserProvider: FC = ({ children }) => {
144
144
 
145
145
  const user = res?.data?.data;
146
146
  if (user?.id == null) {
147
- redirectToLegacySignin(app, getCurrentV2RedirectPath(app, locationRef.current), { replace: true });
147
+ redirectToV2Signin(app, getCurrentV2RedirectPath(app, locationRef.current), { replace: true });
148
148
  return;
149
149
  }
150
150
 
@@ -169,7 +169,7 @@ const CurrentUserProvider: FC = ({ children }) => {
169
169
  } catch (error: any) {
170
170
  const isAuthError = error?.response?.status === 401 || error?.status === 401;
171
171
  if (isAuthError) {
172
- redirectToLegacySignin(app, getCurrentV2RedirectPath(app, locationRef.current), { replace: true });
172
+ redirectToV2Signin(app, getCurrentV2RedirectPath(app, locationRef.current), { replace: true });
173
173
  return;
174
174
  }
175
175
  if (mounted) {
@@ -199,7 +199,7 @@ const RootRedirect: FC = () => {
199
199
 
200
200
  useEffect(() => {
201
201
  if (!hasToken) {
202
- redirectToLegacySignin(app, getDefaultV2AdminRedirectPath(app), { replace: true });
202
+ redirectToV2Signin(app, getDefaultV2AdminRedirectPath(app), { replace: true });
203
203
  }
204
204
  }, [app, hasToken]);
205
205
 
@@ -0,0 +1,21 @@
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 { injectGlobal } from '@emotion/css';
11
+
12
+ // Antd v5 has no labelFontWeight token, so a descendant selector on the
13
+ // stable Form structure is the lowest-risk implementation for the bolder
14
+ // form label default. Injected globally because v1's GlobalThemeProvider
15
+ // re-exports v2's, so any v1 app served from this codebase goes through
16
+ // the same provider and should get the same defaults.
17
+ injectGlobal`
18
+ .ant-form-item-label > label {
19
+ font-weight: 600;
20
+ }
21
+ `;
@@ -13,6 +13,7 @@ import React, { createContext, FC, useCallback, useMemo, useRef } from 'react';
13
13
  import compatOldTheme from './compatOldTheme';
14
14
  import { addCustomAlgorithmToTheme } from './customAlgorithm';
15
15
  import defaultTheme from './defaultTheme';
16
+ import './globalStyles';
16
17
  import { ThemeConfig } from './type';
17
18
 
18
19
  interface ThemeItem {
@@ -33,7 +33,8 @@ import * as ReactRouter from 'react-router';
33
33
  import * as ReactRouterDom from 'react-router-dom';
34
34
  import jsxRuntime from 'react/jsx-runtime';
35
35
  import * as nocobaseClientV2 from '../index';
36
-
36
+ import * as dndKitCore from '@dnd-kit/core';
37
+ import * as dndKitSortable from '@dnd-kit/sortable';
37
38
  import type { RequireJS } from './requirejs';
38
39
 
39
40
  /**
@@ -75,6 +76,9 @@ export function defineGlobalDeps(requirejs: RequireJS) {
75
76
  requirejs.define('@nocobase/evaluators', () => nocobaseEvaluators);
76
77
  requirejs.define('@nocobase/evaluators/client', () => nocobaseEvaluators);
77
78
 
79
+ requirejs.define('@dnd-kit/core', () => dndKitCore);
80
+ requirejs.define('@dnd-kit/sortable', () => dndKitSortable);
81
+
78
82
  // utils
79
83
  requirejs.define('ahooks', () => ahooks);
80
84
  requirejs.define('axios', () => axios);