@nocobase/client-v2 2.1.0-beta.30 → 2.1.0-beta.33
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/PluginManager.d.ts +1 -0
- package/es/components/form/DrawerFormLayout.d.ts +49 -0
- package/es/components/form/EnvVariableInput.d.ts +42 -0
- package/es/components/form/FileSizeInput.d.ts +27 -0
- package/es/components/form/createFormRegistry.d.ts +33 -0
- package/es/components/form/index.d.ts +13 -0
- package/es/components/index.d.ts +1 -1
- package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
- package/es/flow/components/code-editor/index.d.ts +1 -0
- package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
- package/es/flow/models/blocks/shared/legacyDefaultValueMigrationBase.d.ts +1 -0
- package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +4 -0
- package/es/flow/models/fields/ClickableFieldModel.d.ts +3 -0
- package/es/flow/models/fields/DisplayEnumFieldModel.d.ts +6 -0
- package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +2 -2
- package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.mjs +119 -108
- package/es/utils/remotePlugins.d.ts +0 -4
- package/lib/index.js +122 -111
- package/package.json +9 -5
- package/src/BaseApplication.tsx +14 -8
- package/src/PluginManager.ts +1 -0
- package/src/__tests__/app.test.tsx +28 -1
- package/src/__tests__/globalDeps.test.ts +1 -0
- package/src/__tests__/remotePlugins.test.ts +29 -18
- package/src/components/form/DrawerFormLayout.tsx +103 -0
- package/src/components/form/EnvVariableInput.tsx +126 -0
- package/src/components/form/FileSizeInput.tsx +105 -0
- package/src/components/form/createFormRegistry.ts +60 -0
- package/src/components/form/index.tsx +14 -0
- package/src/components/index.ts +1 -1
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
- package/src/flow/actions/__tests__/formAssignRules.legacyMigration.test.tsx +173 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
- package/src/flow/actions/__tests__/pattern.test.ts +134 -0
- package/src/flow/actions/__tests__/titleField.test.ts +45 -0
- package/src/flow/actions/filterFormDefaultValues.tsx +30 -9
- package/src/flow/actions/formAssignRules.tsx +24 -9
- package/src/flow/actions/linkageRules.tsx +240 -258
- package/src/flow/actions/pattern.tsx +41 -6
- package/src/flow/actions/setTargetDataScope.tsx +32 -3
- package/src/flow/actions/titleField.tsx +4 -2
- package/src/flow/actions/validation.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -4
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +13 -5
- package/src/flow/components/DynamicFlowsIcon.tsx +87 -13
- package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +195 -8
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/code-editor/index.tsx +12 -8
- package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
- package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
- package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
- package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
- package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
- package/src/flow/models/base/PageModel/RootPageModel.tsx +1 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
- package/src/flow/models/blocks/filter-form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +2 -8
- package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
- package/src/flow/models/blocks/form/FormItemModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
- package/src/flow/models/blocks/form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
- package/src/flow/models/blocks/shared/legacyDefaultValueMigrationBase.ts +21 -5
- package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
- package/src/flow/models/blocks/table/TableColumnModel.tsx +8 -2
- package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +132 -1
- package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +144 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +170 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +55 -6
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +44 -0
- package/src/flow/models/fields/DisplayTitleFieldModel.tsx +40 -7
- package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +202 -1
- package/src/flow/models/fields/__tests__/DisplayEnumFieldModel.test.tsx +39 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
- package/src/flow/models/utils/displayValueUtils.ts +57 -0
- package/src/utils/globalDeps.ts +11 -0
- package/src/utils/remotePlugins.ts +7 -27
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.33",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -24,18 +24,22 @@
|
|
|
24
24
|
"@formily/antd-v5": "1.2.3",
|
|
25
25
|
"@formily/react": "^2.2.27",
|
|
26
26
|
"@formily/shared": "^2.2.27",
|
|
27
|
-
"@nocobase/
|
|
28
|
-
"@nocobase/
|
|
29
|
-
"@nocobase/
|
|
27
|
+
"@nocobase/evaluators": "2.1.0-beta.33",
|
|
28
|
+
"@nocobase/flow-engine": "2.1.0-beta.33",
|
|
29
|
+
"@nocobase/sdk": "2.1.0-beta.33",
|
|
30
|
+
"@nocobase/shared": "2.1.0-beta.33",
|
|
30
31
|
"ahooks": "^3.7.2",
|
|
31
32
|
"antd": "5.24.2",
|
|
33
|
+
"antd-style": "3.7.1",
|
|
34
|
+
"axios": "^1.7.0",
|
|
32
35
|
"classnames": "^2.3.1",
|
|
33
36
|
"dayjs": "^1.11.10",
|
|
37
|
+
"file-saver": "^2.0.5",
|
|
34
38
|
"i18next": "^22.4.9",
|
|
35
39
|
"json5": "^2.2.3",
|
|
36
40
|
"lodash": "4.17.21",
|
|
37
41
|
"react-i18next": "^11.15.1",
|
|
38
42
|
"react-router-dom": "^6.30.1"
|
|
39
43
|
},
|
|
40
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "4815c394e80a264fa8ed619246280923c47aeb72"
|
|
41
45
|
}
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -586,19 +586,25 @@ export class ApplicationModel extends FlowModel {
|
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
render() {
|
|
589
|
-
if (this.app.maintaining) {
|
|
590
|
-
return this.renderMaintaining()
|
|
589
|
+
if (this.app.maintaining && !this.app.maintained) {
|
|
590
|
+
return <>{this.renderMaintaining()}</>;
|
|
591
591
|
}
|
|
592
|
-
if (this.app.error) {
|
|
593
|
-
return this.renderError()
|
|
592
|
+
if (this.app.error && !this.app.maintaining) {
|
|
593
|
+
return <>{this.renderError()}</>;
|
|
594
594
|
}
|
|
595
|
-
return
|
|
595
|
+
return (
|
|
596
|
+
<>
|
|
597
|
+
{this.renderContent()}
|
|
598
|
+
{this.app.maintaining && this.app.maintained ? this.renderMaintainingDialog() : null}
|
|
599
|
+
</>
|
|
600
|
+
);
|
|
596
601
|
}
|
|
597
602
|
|
|
598
603
|
renderMaintaining() {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
604
|
+
return this.app.renderComponent('AppMaintaining', { app: this.app, error: this.app.error });
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
renderMaintainingDialog() {
|
|
602
608
|
return this.app.renderComponent('AppMaintainingDialog', { app: this.app, error: this.app.error });
|
|
603
609
|
}
|
|
604
610
|
|
package/src/PluginManager.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { Application, createMockClient, NocoBaseBuildInPlugin, Plugin } from '@nocobase/client-v2';
|
|
11
11
|
import { useFlowEngineContext } from '@nocobase/flow-engine';
|
|
12
|
-
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
12
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { Outlet } from 'react-router-dom';
|
|
15
15
|
|
|
@@ -254,6 +254,33 @@ describe('app', () => {
|
|
|
254
254
|
expect(screen.getByText('maintaining dialog message')).toBeInTheDocument();
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
it('should keep current content behind maintained dialog state', async () => {
|
|
258
|
+
const CurrentPage = () => {
|
|
259
|
+
const [count, setCount] = React.useState(0);
|
|
260
|
+
return <button onClick={() => setCount((value) => value + 1)}>Current page count: {count}</button>;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
class PluginHelloClient extends Plugin {
|
|
264
|
+
async load() {
|
|
265
|
+
this.router.add('root', { path: '/', Component: CurrentPage });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const app = createMockClient({ plugins: [PluginHelloClient] });
|
|
269
|
+
await renderApp(app);
|
|
270
|
+
fireEvent.click(screen.getByRole('button', { name: 'Current page count: 0' }));
|
|
271
|
+
expect(screen.getByRole('button', { name: 'Current page count: 1' })).toBeInTheDocument();
|
|
272
|
+
|
|
273
|
+
act(() => {
|
|
274
|
+
app.maintained = true;
|
|
275
|
+
app.maintaining = true;
|
|
276
|
+
app.error = { code: 'APP_COMMANDING', command: { name: 'pm.enable' }, message: 'starting sub applications...' };
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(screen.getByRole('button', { name: 'Current page count: 1' })).toBeInTheDocument();
|
|
280
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
281
|
+
expect(screen.getByText('Enabling plugin')).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
257
284
|
it('should handle long loading state gracefully', async () => {
|
|
258
285
|
class PluginHelloClient extends Plugin {
|
|
259
286
|
async load() {
|
|
@@ -25,6 +25,7 @@ describe('client-v2 defineGlobalDeps', () => {
|
|
|
25
25
|
expect(define).toHaveBeenCalledWith('@nocobase/utils/client', expect.any(Function));
|
|
26
26
|
expect(define).toHaveBeenCalledWith('@nocobase/client-v2', expect.any(Function));
|
|
27
27
|
expect(define).toHaveBeenCalledWith('@nocobase/flow-engine', expect.any(Function));
|
|
28
|
+
expect(define).toHaveBeenCalledWith('@nocobase/evaluators/client', expect.any(Function));
|
|
28
29
|
expect(define).toHaveBeenCalledWith('ahooks', expect.any(Function));
|
|
29
30
|
expect(define).toHaveBeenCalledWith('dayjs', expect.any(Function));
|
|
30
31
|
expect(define).toHaveBeenCalledWith('lodash', expect.any(Function));
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { Plugin } from '../Plugin';
|
|
11
11
|
import { getRequireJs } from '../utils/requirejs';
|
|
12
|
-
import {
|
|
12
|
+
import { configRequirejs, defineDevPlugins, getPlugins } from '../utils/remotePlugins';
|
|
13
13
|
|
|
14
14
|
describe('client-v2 remotePlugins', () => {
|
|
15
15
|
afterEach(() => {
|
|
@@ -29,19 +29,6 @@ describe('client-v2 remotePlugins', () => {
|
|
|
29
29
|
expect(mockDefine).toHaveBeenCalledWith('@nocobase/demo/client-v2', expect.any(Function));
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
it('should define remote plugin proxies with /client-v2 module ids', () => {
|
|
33
|
-
const mockDefine: any = vi.fn();
|
|
34
|
-
window.define = mockDefine;
|
|
35
|
-
|
|
36
|
-
definePluginClient('@nocobase/demo');
|
|
37
|
-
|
|
38
|
-
expect(mockDefine).toHaveBeenCalledWith(
|
|
39
|
-
'@nocobase/demo/client-v2',
|
|
40
|
-
['exports', '@nocobase/demo'],
|
|
41
|
-
expect.any(Function),
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
32
|
it('should not define /client aliases when loading v2 plugins', async () => {
|
|
46
33
|
class DemoPlugin extends Plugin {}
|
|
47
34
|
|
|
@@ -71,16 +58,40 @@ describe('client-v2 remotePlugins', () => {
|
|
|
71
58
|
expect(mockDefine).not.toHaveBeenCalledWith('@nocobase/demo/client', expect.any(Function));
|
|
72
59
|
});
|
|
73
60
|
|
|
61
|
+
it('should configure remote plugin paths with /client-v2 module ids', () => {
|
|
62
|
+
const requirejs: any = {
|
|
63
|
+
requirejs: {
|
|
64
|
+
config: vi.fn(),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
configRequirejs(requirejs, [
|
|
69
|
+
{
|
|
70
|
+
packageName: '@nocobase/demo',
|
|
71
|
+
url: '/static/plugins/@nocobase/demo/dist/client-v2/index.js',
|
|
72
|
+
},
|
|
73
|
+
] as any);
|
|
74
|
+
|
|
75
|
+
expect(requirejs.requirejs.config).toHaveBeenCalledWith({
|
|
76
|
+
waitSeconds: 120,
|
|
77
|
+
paths: {
|
|
78
|
+
'@nocobase/demo/client-v2': '/static/plugins/@nocobase/demo/dist/client-v2/index.js',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
74
83
|
it('should not append duplicate .js for plugin URLs without query strings', () => {
|
|
75
84
|
const requirejs = getRequireJs();
|
|
76
85
|
|
|
77
86
|
requirejs.requirejs.config({
|
|
78
87
|
paths: {
|
|
79
|
-
'@nocobase/demo': '/static/plugins/@nocobase/demo/dist/client-v2/index.js',
|
|
88
|
+
'@nocobase/demo/client-v2': '/static/plugins/@nocobase/demo/dist/client-v2/index.js',
|
|
80
89
|
},
|
|
81
90
|
});
|
|
82
91
|
|
|
83
|
-
expect(requirejs.requirejs.toUrl('@nocobase/demo')).toBe(
|
|
92
|
+
expect(requirejs.requirejs.toUrl('@nocobase/demo/client-v2')).toBe(
|
|
93
|
+
'/static/plugins/@nocobase/demo/dist/client-v2/index.js',
|
|
94
|
+
);
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
it('should keep hashed plugin URLs unchanged', () => {
|
|
@@ -88,11 +99,11 @@ describe('client-v2 remotePlugins', () => {
|
|
|
88
99
|
|
|
89
100
|
requirejs.requirejs.config({
|
|
90
101
|
paths: {
|
|
91
|
-
'@nocobase/demo': '/static/plugins/@nocobase/demo/dist/client-v2/index.js?hash=12345678',
|
|
102
|
+
'@nocobase/demo/client-v2': '/static/plugins/@nocobase/demo/dist/client-v2/index.js?hash=12345678',
|
|
92
103
|
},
|
|
93
104
|
});
|
|
94
105
|
|
|
95
|
-
expect(requirejs.requirejs.toUrl('@nocobase/demo')).toBe(
|
|
106
|
+
expect(requirejs.requirejs.toUrl('@nocobase/demo/client-v2')).toBe(
|
|
96
107
|
'/static/plugins/@nocobase/demo/dist/client-v2/index.js?hash=12345678',
|
|
97
108
|
);
|
|
98
109
|
});
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { CloseOutlined } from '@ant-design/icons';
|
|
11
|
+
import { css } from '@emotion/css';
|
|
12
|
+
import { useFlowView } from '@nocobase/flow-engine';
|
|
13
|
+
import { Button, Space } from 'antd';
|
|
14
|
+
import React, { useCallback } from 'react';
|
|
15
|
+
|
|
16
|
+
export interface DrawerFormLayoutProps {
|
|
17
|
+
/** Header title rendered next to the close (X) button. */
|
|
18
|
+
title: React.ReactNode;
|
|
19
|
+
/** Form body — typically a `<Form>` wrapping `<Form.Item>` fields. */
|
|
20
|
+
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
|
+
/**
|
|
27
|
+
* Called when the Submit button is clicked. Caller owns validation + the
|
|
28
|
+
* actual API call; the drawer is closed automatically when `onSubmit`
|
|
29
|
+
* resolves. Throw from `onSubmit` to keep the drawer open (e.g. on a
|
|
30
|
+
* validation error).
|
|
31
|
+
*/
|
|
32
|
+
onSubmit?: () => void | Promise<void>;
|
|
33
|
+
/** Drives the Submit button's loading state. */
|
|
34
|
+
submitting?: boolean;
|
|
35
|
+
/** Override the Submit button label. Defaults to "Submit". */
|
|
36
|
+
submitText?: React.ReactNode;
|
|
37
|
+
/** Override the Cancel button label. Defaults to "Cancel". */
|
|
38
|
+
cancelText?: React.ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* Full override of the footer content. When provided, the default
|
|
41
|
+
* Cancel + Submit buttons are replaced. Useful for forms that need
|
|
42
|
+
* extra actions (e.g. Preview, Save draft).
|
|
43
|
+
*/
|
|
44
|
+
footer?: React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const titleClassName = css`
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 8px;
|
|
51
|
+
margin-left: -8px;
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Standard layout for drawer-hosted forms: a close-icon + title header on
|
|
56
|
+
* top, the caller-provided form body in the middle, and a Cancel + Submit
|
|
57
|
+
* footer at the bottom. Wraps `useFlowView()`'s `Header` / `Footer` slots
|
|
58
|
+
* so the drawer chrome stays consistent across plugins.
|
|
59
|
+
*
|
|
60
|
+
* Callers own the `<Form>` instance, validation, and the actual API call.
|
|
61
|
+
* This component only handles the chrome and the close behaviour.
|
|
62
|
+
*/
|
|
63
|
+
export function DrawerFormLayout(props: DrawerFormLayoutProps) {
|
|
64
|
+
const view = useFlowView();
|
|
65
|
+
|
|
66
|
+
const handleCancel = useCallback(async () => {
|
|
67
|
+
await props.onCancel?.();
|
|
68
|
+
await view.close();
|
|
69
|
+
}, [props, view]);
|
|
70
|
+
|
|
71
|
+
const handleSubmit = useCallback(async () => {
|
|
72
|
+
await props.onSubmit?.();
|
|
73
|
+
await view.close();
|
|
74
|
+
}, [props, view]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<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}
|
|
88
|
+
{props.children}
|
|
89
|
+
{view.Footer ? (
|
|
90
|
+
<view.Footer>
|
|
91
|
+
{props.footer ?? (
|
|
92
|
+
<Space>
|
|
93
|
+
<Button onClick={handleCancel}>{props.cancelText ?? 'Cancel'}</Button>
|
|
94
|
+
<Button type="primary" loading={props.submitting} onClick={handleSubmit}>
|
|
95
|
+
{props.submitText ?? 'Submit'}
|
|
96
|
+
</Button>
|
|
97
|
+
</Space>
|
|
98
|
+
)}
|
|
99
|
+
</view.Footer>
|
|
100
|
+
) : null}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 {
|
|
11
|
+
useFlowContext,
|
|
12
|
+
VariableHybridInput,
|
|
13
|
+
type MetaTreeNode,
|
|
14
|
+
type VariableHybridInputConverters,
|
|
15
|
+
} from '@nocobase/flow-engine';
|
|
16
|
+
import { useRequest } from 'ahooks';
|
|
17
|
+
import { Input } from 'antd';
|
|
18
|
+
import React, { useMemo } from 'react';
|
|
19
|
+
|
|
20
|
+
const ENV_EXPR_REGEXP = /\{\{\s*(\$env\.[^{}]+?)\s*\}\}/g;
|
|
21
|
+
const ENV_SINGLE_EXPR_REGEXP = /^\{\{\s*(\$env\.[^{}]+?)\s*\}\}$/;
|
|
22
|
+
const META_TREE_CACHE_KEY = '@nocobase/client-v2:EnvVariableInput:metaTree';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert a stored value like `"{{ $env.foo.bar }}"` back into the
|
|
26
|
+
* `[$env, foo, bar]` path used by the variable picker.
|
|
27
|
+
*/
|
|
28
|
+
export function parseEnvPath(value?: string): string[] | undefined {
|
|
29
|
+
const matched = value?.trim().match(ENV_SINGLE_EXPR_REGEXP);
|
|
30
|
+
return matched?.[1] ? matched[1].split('.') : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format a meta tree node back into a `"{{ $env.x.y }}"` server-compatible
|
|
35
|
+
* expression. Used as the `formatPathToValue` converter so the picker output
|
|
36
|
+
* survives a round trip through the API.
|
|
37
|
+
*/
|
|
38
|
+
export function formatEnvPath(meta?: MetaTreeNode) {
|
|
39
|
+
const paths = meta?.paths || [];
|
|
40
|
+
if (paths[0] !== '$env' || paths.length < 2) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return `{{ ${paths.join('.')} }}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pull the `$env` sub-tree off the FlowContext meta registry and eagerly
|
|
48
|
+
* resolve lazy `children` thunks so the picker can render labels on first
|
|
49
|
+
* paint. Empty tree (no env-variables plugin or no defined vars) yields `[]`.
|
|
50
|
+
*/
|
|
51
|
+
function useEnvMetaTree(): MetaTreeNode[] {
|
|
52
|
+
const ctx = useFlowContext();
|
|
53
|
+
const { data } = useRequest<MetaTreeNode[], []>(
|
|
54
|
+
async () => {
|
|
55
|
+
const tree = ctx.getPropertyMetaTree().filter((node) => node.name === '$env');
|
|
56
|
+
for (const node of tree) {
|
|
57
|
+
if (typeof node.children === 'function') {
|
|
58
|
+
try {
|
|
59
|
+
const resolved = await (node.children as () => Promise<MetaTreeNode[]>)();
|
|
60
|
+
node.children = Array.isArray(resolved) ? resolved : [];
|
|
61
|
+
} catch {
|
|
62
|
+
node.children = [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return tree.filter((node) => Array.isArray(node.children) && node.children.length > 0);
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
cacheKey: META_TREE_CACHE_KEY,
|
|
70
|
+
refreshOnWindowFocus: true,
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return data ?? [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface EnvVariableInputProps {
|
|
78
|
+
value?: string;
|
|
79
|
+
onChange?: (value: string) => void;
|
|
80
|
+
addonBefore?: React.ReactNode;
|
|
81
|
+
disabled?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* When true, plain (non-variable) values are masked via `Input.Password`
|
|
84
|
+
* so secret credentials are not displayed verbatim. Variable expressions
|
|
85
|
+
* remain editable through the variable picker even in password mode.
|
|
86
|
+
*/
|
|
87
|
+
password?: boolean;
|
|
88
|
+
placeholder?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isVariableExpr = (value?: string) => typeof value === 'string' && /\{\{\s*[^{}]+?\s*\}\}/.test(value);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generic input component for fields that accept either a literal value or a
|
|
95
|
+
* `{{ $env.X }}` reference. The `$env` namespace is wired through the
|
|
96
|
+
* environment-variables plugin's `flowEngine.context.defineProperty('$env', ...)`
|
|
97
|
+
* registration; this component is the single consumption point and degrades
|
|
98
|
+
* gracefully to a plain text input when no env variables are defined.
|
|
99
|
+
*/
|
|
100
|
+
export function EnvVariableInput(props: EnvVariableInputProps) {
|
|
101
|
+
const { password, ...rest } = props;
|
|
102
|
+
const metaTree = useEnvMetaTree();
|
|
103
|
+
|
|
104
|
+
const converters = useMemo<VariableHybridInputConverters>(
|
|
105
|
+
() => ({
|
|
106
|
+
formatPathToValue: formatEnvPath,
|
|
107
|
+
parseValueToPath: parseEnvPath,
|
|
108
|
+
variableRegExp: ENV_EXPR_REGEXP,
|
|
109
|
+
}),
|
|
110
|
+
[],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (password && rest.value && !isVariableExpr(rest.value)) {
|
|
114
|
+
return (
|
|
115
|
+
<Input.Password
|
|
116
|
+
disabled={rest.disabled}
|
|
117
|
+
placeholder={rest.placeholder}
|
|
118
|
+
value={rest.value}
|
|
119
|
+
onChange={(event) => rest.onChange?.(event.target.value)}
|
|
120
|
+
autoFocus
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return <VariableHybridInput {...rest} converters={converters} metaTree={metaTree} />;
|
|
126
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 } from '@emotion/css';
|
|
11
|
+
import { InputNumber, Select, Space } from 'antd';
|
|
12
|
+
import React, { useCallback } from 'react';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MIN = 1;
|
|
15
|
+
const DEFAULT_MAX = Number.POSITIVE_INFINITY;
|
|
16
|
+
const DEFAULT_VALUE = 1024 * 1024 * 20;
|
|
17
|
+
|
|
18
|
+
const UNIT_OPTIONS = [
|
|
19
|
+
{ value: 1, label: 'Byte' },
|
|
20
|
+
{ value: 1024, label: 'KB' },
|
|
21
|
+
{ value: 1024 * 1024, label: 'MB' },
|
|
22
|
+
{ value: 1024 * 1024 * 1024, label: 'GB' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Mirrors v1's `.auto-width` rule registered globally on FormItem: shrink the
|
|
26
|
+
// antd control to its content width while keeping a sensible minimum.
|
|
27
|
+
const autoWidthClassName = css`
|
|
28
|
+
&.ant-input-number,
|
|
29
|
+
&.ant-select {
|
|
30
|
+
width: auto;
|
|
31
|
+
min-width: 6em;
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
function getUnitOption(value: number, defaultUnit = 1024 * 1024) {
|
|
36
|
+
const size = value || defaultUnit;
|
|
37
|
+
for (let i = UNIT_OPTIONS.length - 1; i >= 0; i -= 1) {
|
|
38
|
+
const option = UNIT_OPTIONS[i];
|
|
39
|
+
if (size % option.value === 0) {
|
|
40
|
+
return option;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return UNIT_OPTIONS[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clampSize(value: number, min: number, max: number) {
|
|
47
|
+
return Math.min(Math.max(min, value), max);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FileSizeInputProps {
|
|
51
|
+
value?: number;
|
|
52
|
+
onChange?: (value?: number) => void;
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
/** Minimum byte size. Empty / below-min input snaps to this on blur. Defaults to 1. */
|
|
55
|
+
min?: number;
|
|
56
|
+
/** Maximum byte size. Defaults to `Number.POSITIVE_INFINITY`. */
|
|
57
|
+
max?: number;
|
|
58
|
+
/** Default byte size used to derive the initial unit shown when the field is empty. Defaults to 20 MB. */
|
|
59
|
+
defaultValue?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Byte-valued size input paired with a unit selector (Byte / KB / MB / GB).
|
|
64
|
+
* The persisted value is always normalized to bytes; the displayed number is
|
|
65
|
+
* derived from the picked unit. Useful for fields like file-size limits or
|
|
66
|
+
* memory quotas where the natural input unit varies by magnitude.
|
|
67
|
+
*/
|
|
68
|
+
export function FileSizeInput(props: FileSizeInputProps) {
|
|
69
|
+
const min = props.min ?? DEFAULT_MIN;
|
|
70
|
+
const max = props.max ?? DEFAULT_MAX;
|
|
71
|
+
const defaultValue = props.defaultValue ?? DEFAULT_VALUE;
|
|
72
|
+
const unit = getUnitOption(props.value ?? defaultValue);
|
|
73
|
+
const value = props.value == null ? props.value : props.value / unit.value;
|
|
74
|
+
|
|
75
|
+
const handleBlur = useCallback(() => {
|
|
76
|
+
if (props.value == null || props.value < min) {
|
|
77
|
+
props.onChange?.(min);
|
|
78
|
+
}
|
|
79
|
+
}, [props, min]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Space.Compact>
|
|
83
|
+
<InputNumber
|
|
84
|
+
value={value}
|
|
85
|
+
disabled={props.disabled}
|
|
86
|
+
defaultValue={defaultValue / getUnitOption(defaultValue).value}
|
|
87
|
+
step={1}
|
|
88
|
+
className={autoWidthClassName}
|
|
89
|
+
onBlur={handleBlur}
|
|
90
|
+
onChange={(nextValue) => {
|
|
91
|
+
props.onChange?.(nextValue == null ? undefined : clampSize(Number(nextValue) * unit.value, min, max));
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
<Select
|
|
95
|
+
disabled={props.disabled}
|
|
96
|
+
options={UNIT_OPTIONS}
|
|
97
|
+
value={unit.value}
|
|
98
|
+
className={autoWidthClassName}
|
|
99
|
+
onChange={(nextUnit) => {
|
|
100
|
+
props.onChange?.(value == null ? undefined : clampSize(Number(value) * nextUnit, min, max));
|
|
101
|
+
}}
|
|
102
|
+
/>
|
|
103
|
+
</Space.Compact>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
export interface FormRegistryEntry {
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FormRegistry<T extends FormRegistryEntry> {
|
|
15
|
+
readonly namespace: string;
|
|
16
|
+
register(entry: T): void;
|
|
17
|
+
unregister(name: string): boolean;
|
|
18
|
+
get(name: string): T | undefined;
|
|
19
|
+
has(name: string): boolean;
|
|
20
|
+
list(): T[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create an isolated, namespaced registry of form-like entries.
|
|
25
|
+
*
|
|
26
|
+
* Each call returns a fresh registry instance backed by its own closure-scoped
|
|
27
|
+
* `Map`, so plugins do not share state across namespaces. Plugins that need an
|
|
28
|
+
* extension point for form-shaped registrations (e.g. storage configuration
|
|
29
|
+
* forms, data-source connection forms) can build their own typed registry on
|
|
30
|
+
* top of this primitive instead of re-implementing the same boilerplate.
|
|
31
|
+
*
|
|
32
|
+
* Re-registering the same `name` overwrites the previous entry and emits a
|
|
33
|
+
* console warning. This is intentional so HMR / hot reload works without
|
|
34
|
+
* throwing, while still surfacing unintended duplicates during development.
|
|
35
|
+
*/
|
|
36
|
+
export function createFormRegistry<T extends FormRegistryEntry>(namespace: string): FormRegistry<T> {
|
|
37
|
+
const entries = new Map<string, T>();
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
namespace,
|
|
41
|
+
register(entry) {
|
|
42
|
+
if (entries.has(entry.name)) {
|
|
43
|
+
console.warn(`[${namespace}] entry "${entry.name}" already registered, overwriting.`);
|
|
44
|
+
}
|
|
45
|
+
entries.set(entry.name, entry);
|
|
46
|
+
},
|
|
47
|
+
unregister(name) {
|
|
48
|
+
return entries.delete(name);
|
|
49
|
+
},
|
|
50
|
+
get(name) {
|
|
51
|
+
return entries.get(name);
|
|
52
|
+
},
|
|
53
|
+
has(name) {
|
|
54
|
+
return entries.has(name);
|
|
55
|
+
},
|
|
56
|
+
list() {
|
|
57
|
+
return Array.from(entries.values());
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export * from './createFormRegistry';
|
|
11
|
+
export * from './DrawerFormLayout';
|
|
12
|
+
export * from './EnvVariableInput';
|
|
13
|
+
export * from './FileSizeInput';
|
|
14
|
+
export * from './JsonTextArea';
|