@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.29
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/components/form/JsonTextArea.d.ts +18 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/dateRangeLimit.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -0
- package/es/flow/components/code-editor/types.d.ts +1 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
- package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
- package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
- package/es/flow-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -1
- package/es/index.mjs +97 -90
- package/lib/index.js +99 -92
- package/package.json +6 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/components/form/JsonTextArea.tsx +129 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
- package/src/flow/actions/__tests__/pattern.test.ts +190 -0
- package/src/flow/actions/dateRangeLimit.tsx +66 -0
- package/src/flow/actions/index.ts +1 -0
- package/src/flow/actions/linkageRules.tsx +117 -19
- package/src/flow/actions/openView.tsx +2 -1
- package/src/flow/actions/pattern.tsx +25 -2
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
- package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
- package/src/flow/components/code-editor/index.tsx +18 -17
- package/src/flow/components/code-editor/linter.ts +222 -158
- package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
- package/src/flow/components/code-editor/types.ts +1 -0
- package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
- package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
- package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
- package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
- package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
- package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
- package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
- package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
- package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
- package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
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 { autorun } from '@formily/reactive';
|
|
11
|
+
import { Form } from 'antd';
|
|
12
|
+
import { useFlowContext } from '@nocobase/flow-engine';
|
|
13
|
+
import { dayjs } from '@nocobase/utils/client';
|
|
14
|
+
import { first, last } from 'lodash';
|
|
15
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
16
|
+
|
|
17
|
+
type DateLimitProps = {
|
|
18
|
+
_minDate?: any;
|
|
19
|
+
_maxDate?: any;
|
|
20
|
+
currentForm?: any;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function useDateLimit(props: DateLimitProps) {
|
|
24
|
+
const ctx = useFlowContext();
|
|
25
|
+
const isAntdFormInstance = typeof props.currentForm?.getFieldsValue === 'function';
|
|
26
|
+
const currentFormValues = Form.useWatch([], isAntdFormInstance ? props.currentForm : undefined);
|
|
27
|
+
const [minDate, setMinDate] = useState<dayjs.Dayjs | null>(null);
|
|
28
|
+
const [maxDate, setMaxDate] = useState<dayjs.Dayjs | null>(null);
|
|
29
|
+
const [disabledDate, setDisabledDate] = useState<any>(null);
|
|
30
|
+
const [disabledTime, setDisabledTime] = useState<any>(null);
|
|
31
|
+
const disposeRef = useRef<any>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (disposeRef.current) {
|
|
35
|
+
disposeRef.current();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
disposeRef.current = autorun(() => {
|
|
39
|
+
void limitDate();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
disposeRef.current?.();
|
|
44
|
+
};
|
|
45
|
+
}, [ctx, currentFormValues, props._maxDate, props._minDate]);
|
|
46
|
+
|
|
47
|
+
const limitDate = async () => {
|
|
48
|
+
const resolvedParams = await ctx.resolveJsonTemplate({
|
|
49
|
+
_minDate: props._minDate,
|
|
50
|
+
_maxDate: props._maxDate,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const nextMinRaw = Array.isArray(resolvedParams?._minDate)
|
|
54
|
+
? first(resolvedParams._minDate)
|
|
55
|
+
: resolvedParams?._minDate;
|
|
56
|
+
const nextMaxRaw = Array.isArray(resolvedParams?._maxDate)
|
|
57
|
+
? last(resolvedParams._maxDate)
|
|
58
|
+
: resolvedParams?._maxDate;
|
|
59
|
+
const nextMinDate = nextMinRaw ? dayjs(nextMinRaw) : null;
|
|
60
|
+
const nextMaxDate = nextMaxRaw ? dayjs(nextMaxRaw) : null;
|
|
61
|
+
|
|
62
|
+
setMinDate(nextMinDate?.isValid?.() ? nextMinDate : null);
|
|
63
|
+
setMaxDate(nextMaxDate?.isValid?.() ? nextMaxDate : null);
|
|
64
|
+
|
|
65
|
+
const fullTimeArr = Array.from({ length: 60 }, (_, i) => i);
|
|
66
|
+
|
|
67
|
+
const nextDisabledDate = (current: dayjs.Dayjs) => {
|
|
68
|
+
if (!dayjs.isDayjs(current)) return false;
|
|
69
|
+
|
|
70
|
+
const min = nextMinDate?.isValid?.() ? nextMinDate.startOf('day') : null;
|
|
71
|
+
const max = nextMaxDate?.isValid?.() ? nextMaxDate.endOf('day') : null;
|
|
72
|
+
|
|
73
|
+
if (min && current.startOf('day').isBefore(min)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (max && current.startOf('day').isAfter(max)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const nextDisabledTime = (current: dayjs.Dayjs) => {
|
|
83
|
+
if (!current || (!nextMinDate?.isValid?.() && !nextMaxDate?.isValid?.())) {
|
|
84
|
+
return { disabledHours: () => [], disabledMinutes: () => [], disabledSeconds: () => [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const isCurrentMinDay = !!nextMinDate?.isValid?.() && current.isSame(nextMinDate, 'day');
|
|
88
|
+
const isCurrentMaxDay = !!nextMaxDate?.isValid?.() && current.isSame(nextMaxDate, 'day');
|
|
89
|
+
|
|
90
|
+
const disabledHours = () => {
|
|
91
|
+
const hours = [];
|
|
92
|
+
if (isCurrentMinDay && nextMinDate) {
|
|
93
|
+
for (let hour = 0; hour < nextMinDate.hour(); hour++) {
|
|
94
|
+
hours.push(hour);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (isCurrentMaxDay && nextMaxDate) {
|
|
98
|
+
for (let hour = nextMaxDate.hour() + 1; hour < 24; hour++) {
|
|
99
|
+
hours.push(hour);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return hours;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const disabledMinutes = (selectedHour: number) => {
|
|
106
|
+
if (isCurrentMinDay && nextMinDate && selectedHour === nextMinDate.hour()) {
|
|
107
|
+
return fullTimeArr.filter((minute) => minute < nextMinDate.minute());
|
|
108
|
+
}
|
|
109
|
+
if (isCurrentMaxDay && nextMaxDate && selectedHour === nextMaxDate.hour()) {
|
|
110
|
+
return fullTimeArr.filter((minute) => minute > nextMaxDate.minute());
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const disabledSeconds = (selectedHour: number, selectedMinute: number) => {
|
|
116
|
+
if (
|
|
117
|
+
isCurrentMinDay &&
|
|
118
|
+
nextMinDate &&
|
|
119
|
+
selectedHour === nextMinDate.hour() &&
|
|
120
|
+
selectedMinute === nextMinDate.minute()
|
|
121
|
+
) {
|
|
122
|
+
return fullTimeArr.filter((second) => second < nextMinDate.second());
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
isCurrentMaxDay &&
|
|
126
|
+
nextMaxDate &&
|
|
127
|
+
selectedHour === nextMaxDate.hour() &&
|
|
128
|
+
selectedMinute === nextMaxDate.minute()
|
|
129
|
+
) {
|
|
130
|
+
return fullTimeArr.filter((second) => second > nextMaxDate.second());
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
disabledHours,
|
|
137
|
+
disabledMinutes,
|
|
138
|
+
disabledSeconds,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
setDisabledDate(() => nextDisabledDate);
|
|
143
|
+
setDisabledTime(() => nextDisabledTime);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
minDate,
|
|
148
|
+
maxDate,
|
|
149
|
+
disabledDate,
|
|
150
|
+
disabledTime,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -8,25 +8,40 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FormItem } from '@nocobase/flow-engine';
|
|
11
|
-
import { Divider } from 'antd';
|
|
11
|
+
import { Divider, theme } from 'antd';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { CommonItemModel } from '../base/CommonItemModel';
|
|
14
14
|
import { NBColorPicker } from './ColorFieldModel';
|
|
15
15
|
|
|
16
|
+
const resolveThemeColor = (value: string | undefined, fallback: string) => {
|
|
17
|
+
return value ? value : fallback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DividerItem = (props: any) => {
|
|
21
|
+
const { token } = theme.useToken();
|
|
22
|
+
const { color, borderColor, label, orientation, dashed } = props;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Divider
|
|
26
|
+
type="horizontal"
|
|
27
|
+
style={{
|
|
28
|
+
color: resolveThemeColor(color, token.colorText),
|
|
29
|
+
borderColor: resolveThemeColor(borderColor, token.colorSplit),
|
|
30
|
+
}}
|
|
31
|
+
orientationMargin="0"
|
|
32
|
+
orientation={orientation}
|
|
33
|
+
dashed={dashed}
|
|
34
|
+
>
|
|
35
|
+
{label}
|
|
36
|
+
</Divider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
16
40
|
export class DividerItemModel extends CommonItemModel {
|
|
17
41
|
render() {
|
|
18
|
-
const { color, borderColor, label, orientation, dashed } = this.props;
|
|
19
42
|
return (
|
|
20
43
|
<FormItem shouldUpdate showLabel={false}>
|
|
21
|
-
<
|
|
22
|
-
type="horizontal"
|
|
23
|
-
style={{ color, borderColor }}
|
|
24
|
-
orientationMargin="0"
|
|
25
|
-
orientation={orientation}
|
|
26
|
-
dashed={dashed}
|
|
27
|
-
>
|
|
28
|
-
{label}
|
|
29
|
-
</Divider>
|
|
44
|
+
<DividerItem {...this.props} />
|
|
30
45
|
</FormItem>
|
|
31
46
|
);
|
|
32
47
|
}
|
|
@@ -38,12 +53,12 @@ DividerItemModel.registerFlow({
|
|
|
38
53
|
steps: {
|
|
39
54
|
title: {
|
|
40
55
|
title: '{{t("Edit divider")}}',
|
|
41
|
-
defaultParams: {
|
|
56
|
+
defaultParams: (ctx) => ({
|
|
42
57
|
label: '{{t("Text")}}',
|
|
43
58
|
orientation: 'left',
|
|
44
|
-
color:
|
|
45
|
-
borderColor:
|
|
46
|
-
},
|
|
59
|
+
color: ctx.themeToken?.colorText,
|
|
60
|
+
borderColor: ctx.themeToken?.colorSplit,
|
|
61
|
+
}),
|
|
47
62
|
uiSchema(ctx) {
|
|
48
63
|
return {
|
|
49
64
|
label: {
|
|
@@ -35,6 +35,10 @@ function JsEditableField() {
|
|
|
35
35
|
ctx.setValue?.(v);
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
if (ctx.readOnly) {
|
|
39
|
+
return <span>{String(value ?? '')}</span>;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
return (
|
|
39
43
|
<Input
|
|
40
44
|
{...ctx.model.props}
|
|
@@ -48,6 +52,23 @@ function JsEditableField() {
|
|
|
48
52
|
ctx.render(<JsEditableField />);
|
|
49
53
|
`;
|
|
50
54
|
|
|
55
|
+
function getEffectivePattern(model?: JSEditableFieldModel) {
|
|
56
|
+
return (
|
|
57
|
+
model?.props?.pattern ??
|
|
58
|
+
(model?.context as { pattern?: string } | undefined)?.pattern ??
|
|
59
|
+
(model?.parent as { props?: { pattern?: string } } | undefined)?.props?.pattern
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isReadOnlyMode(model?: JSEditableFieldModel) {
|
|
64
|
+
return !!model?.props?.readOnly || getEffectivePattern(model) === 'readPretty';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveScriptCode(codeParam?: string) {
|
|
68
|
+
const raw = codeParam ?? DEFAULT_CODE;
|
|
69
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
const JSFormRuntime: React.FC<{
|
|
52
73
|
model: JSEditableFieldModel;
|
|
53
74
|
value?: any;
|
|
@@ -66,26 +87,27 @@ const JSFormRuntime: React.FC<{
|
|
|
66
87
|
// 统一获取&裁剪脚本代码,直接依赖具体 code 字符串,避免顶层 stepParams 引用未变化导致不更新
|
|
67
88
|
const codeParam = model.getStepParams('jsSettings', 'runJs')?.code as string | undefined;
|
|
68
89
|
const scriptCode = useMemo(() => {
|
|
69
|
-
|
|
70
|
-
return typeof raw === 'string' ? raw.trim() : '';
|
|
90
|
+
return resolveScriptCode(codeParam);
|
|
71
91
|
}, [codeParam]);
|
|
72
92
|
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!containerRef.current || !scriptCode) return;
|
|
95
|
+
model.scheduleApplyJsSettings();
|
|
96
|
+
}, [model, scriptCode]);
|
|
97
|
+
|
|
73
98
|
useEffect(() => {
|
|
74
99
|
if (!containerRef.current || !scriptCode) return;
|
|
75
100
|
const event = new CustomEvent('js-field:value-change', { detail: value });
|
|
76
101
|
containerRef.current.dispatchEvent(event);
|
|
77
102
|
}, [value, scriptCode]);
|
|
78
103
|
|
|
79
|
-
|
|
104
|
+
if (readOnly && !scriptCode) {
|
|
105
|
+
return <span>{String(value ?? '')}</span>;
|
|
106
|
+
}
|
|
107
|
+
|
|
80
108
|
if (!scriptCode) {
|
|
81
109
|
return (
|
|
82
|
-
<Input
|
|
83
|
-
value={value}
|
|
84
|
-
onChange={(e) => onChange?.(e.target.value)}
|
|
85
|
-
disabled={disabled}
|
|
86
|
-
readOnly={readOnly}
|
|
87
|
-
style={{ width: '100%' }}
|
|
88
|
-
/>
|
|
110
|
+
<Input value={value} onChange={(e) => onChange?.(e.target.value)} disabled={disabled} style={{ width: '100%' }} />
|
|
89
111
|
);
|
|
90
112
|
}
|
|
91
113
|
|
|
@@ -100,14 +122,77 @@ const JSFormRuntime: React.FC<{
|
|
|
100
122
|
*/
|
|
101
123
|
export class JSEditableFieldModel extends FieldModel {
|
|
102
124
|
private _mountedOnce = false;
|
|
125
|
+
private _pendingJsSettingsApply = false;
|
|
126
|
+
private _lastAppliedJsSettings?: {
|
|
127
|
+
code: string;
|
|
128
|
+
disabled: boolean;
|
|
129
|
+
readOnly: boolean;
|
|
130
|
+
element: HTMLSpanElement | null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
scheduleApplyJsSettings() {
|
|
134
|
+
const codeParam = this.getStepParams('jsSettings', 'runJs')?.code as string | undefined;
|
|
135
|
+
if (!resolveScriptCode(codeParam)) return;
|
|
136
|
+
|
|
137
|
+
if (this._pendingJsSettingsApply) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this._pendingJsSettingsApply = true;
|
|
142
|
+
queueMicrotask(() => {
|
|
143
|
+
this._pendingJsSettingsApply = false;
|
|
144
|
+
|
|
145
|
+
const nextCodeParam = this.getStepParams('jsSettings', 'runJs')?.code as string | undefined;
|
|
146
|
+
const nextCode = resolveScriptCode(nextCodeParam);
|
|
147
|
+
const nextElement = this.context.ref?.current as HTMLSpanElement | null;
|
|
148
|
+
if (!nextCode || !nextElement) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nextRun = {
|
|
153
|
+
code: nextCode,
|
|
154
|
+
disabled: !!this.props?.disabled,
|
|
155
|
+
readOnly: isReadOnlyMode(this),
|
|
156
|
+
element: nextElement,
|
|
157
|
+
};
|
|
158
|
+
const lastRun = this._lastAppliedJsSettings;
|
|
159
|
+
if (
|
|
160
|
+
lastRun &&
|
|
161
|
+
lastRun.code === nextRun.code &&
|
|
162
|
+
lastRun.disabled === nextRun.disabled &&
|
|
163
|
+
lastRun.readOnly === nextRun.readOnly &&
|
|
164
|
+
lastRun.element === nextRun.element
|
|
165
|
+
) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._lastAppliedJsSettings = nextRun;
|
|
170
|
+
void this.applyFlow('jsSettings');
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
useHooksBeforeRender() {
|
|
175
|
+
const codeParam = this.getStepParams('jsSettings', 'runJs')?.code as string | undefined;
|
|
176
|
+
const scriptCode = resolveScriptCode(codeParam);
|
|
177
|
+
const disabled = this.props?.disabled;
|
|
178
|
+
|
|
179
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!scriptCode) return;
|
|
182
|
+
this.scheduleApplyJsSettings();
|
|
183
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
184
|
+
}, [scriptCode, disabled]);
|
|
185
|
+
}
|
|
186
|
+
|
|
103
187
|
render() {
|
|
188
|
+
const readOnly = isReadOnlyMode(this);
|
|
104
189
|
return (
|
|
105
190
|
<JSFormRuntime
|
|
106
191
|
model={this as JSEditableFieldModel}
|
|
107
192
|
value={this.props?.value}
|
|
108
193
|
onChange={this.props?.onChange}
|
|
109
194
|
disabled={this.props?.disabled}
|
|
110
|
-
readOnly={
|
|
195
|
+
readOnly={readOnly}
|
|
111
196
|
/>
|
|
112
197
|
);
|
|
113
198
|
}
|
|
@@ -128,13 +213,17 @@ export class JSEditableFieldModel extends FieldModel {
|
|
|
128
213
|
}
|
|
129
214
|
}
|
|
130
215
|
|
|
131
|
-
|
|
216
|
+
const jsEditableFieldModelMeta = {
|
|
132
217
|
label: tExpr('JS field'),
|
|
133
|
-
|
|
218
|
+
preserveOnPatternChange: true,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
JSEditableFieldModel.define(jsEditableFieldModelMeta);
|
|
134
222
|
|
|
135
223
|
JSEditableFieldModel.registerFlow({
|
|
136
224
|
key: 'jsSettings',
|
|
137
225
|
title: tExpr('JavaScript settings'),
|
|
226
|
+
manual: true,
|
|
138
227
|
steps: {
|
|
139
228
|
runJs: {
|
|
140
229
|
title: tExpr('Write JavaScript'),
|
|
@@ -174,6 +263,10 @@ JSEditableFieldModel.registerFlow({
|
|
|
174
263
|
},
|
|
175
264
|
async handler(ctx, params) {
|
|
176
265
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
266
|
+
if (!code?.trim()) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
177
270
|
ctx.onRefReady(ctx.ref, async (element) => {
|
|
178
271
|
// 暴露容器与读写能力(使用动态 getter 绑定 ref.current,避免容器变更失效)
|
|
179
272
|
ctx.defineProperty('element', {
|
|
@@ -201,7 +294,10 @@ JSEditableFieldModel.registerFlow({
|
|
|
201
294
|
});
|
|
202
295
|
ctx.defineProperty('namePath', { get: () => ctx.model.props?.name, cache: false });
|
|
203
296
|
ctx.defineProperty('disabled', { get: () => !!ctx.model.props?.disabled, cache: false });
|
|
204
|
-
ctx.defineProperty('readOnly', {
|
|
297
|
+
ctx.defineProperty('readOnly', {
|
|
298
|
+
get: () => isReadOnlyMode(ctx.model),
|
|
299
|
+
cache: false,
|
|
300
|
+
});
|
|
205
301
|
const navigator = createSafeNavigator();
|
|
206
302
|
await ctx.runjs(
|
|
207
303
|
code,
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { FlowEngine } from '@nocobase/flow-engine';
|
|
12
|
+
import { ClickableFieldModel } from '../ClickableFieldModel';
|
|
13
|
+
|
|
14
|
+
function createRolesFieldModel(sourceRecord: Record<string, any>) {
|
|
15
|
+
const engine = new FlowEngine();
|
|
16
|
+
engine.registerModels({ ClickableFieldModel });
|
|
17
|
+
|
|
18
|
+
const usersCollection = {
|
|
19
|
+
name: 'users',
|
|
20
|
+
filterTargetKey: 'id',
|
|
21
|
+
};
|
|
22
|
+
const rolesCollection = {
|
|
23
|
+
name: 'roles',
|
|
24
|
+
filterTargetKey: 'name',
|
|
25
|
+
};
|
|
26
|
+
const rolesField = {
|
|
27
|
+
name: 'roles',
|
|
28
|
+
target: 'roles',
|
|
29
|
+
targetKey: 'name',
|
|
30
|
+
type: 'belongsToMany',
|
|
31
|
+
interface: 'm2m',
|
|
32
|
+
collection: usersCollection,
|
|
33
|
+
targetCollection: rolesCollection,
|
|
34
|
+
isAssociationField: () => true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const model = engine.createModel<ClickableFieldModel>({
|
|
38
|
+
use: ClickableFieldModel,
|
|
39
|
+
uid: `clickable-roles-${sourceRecord?.id ?? 'new'}`,
|
|
40
|
+
});
|
|
41
|
+
model.context.defineProperty('collectionField', { value: rolesField });
|
|
42
|
+
model.context.defineProperty('blockModel', { value: { collection: usersCollection } });
|
|
43
|
+
model.context.defineProperty('record', { value: sourceRecord });
|
|
44
|
+
|
|
45
|
+
const dispatchEvent = vi.spyOn(model, 'dispatchEvent').mockResolvedValue([]);
|
|
46
|
+
return { model, dispatchEvent };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('ClickableFieldModel', () => {
|
|
50
|
+
it('opens an association display value as a normal target record when source record has no id', () => {
|
|
51
|
+
const { model, dispatchEvent } = createRolesFieldModel({});
|
|
52
|
+
const event = { type: 'click' };
|
|
53
|
+
|
|
54
|
+
model.onClick(event, { name: 'admin', title: 'Admin' });
|
|
55
|
+
|
|
56
|
+
expect(dispatchEvent).toHaveBeenCalledWith(
|
|
57
|
+
'click',
|
|
58
|
+
{
|
|
59
|
+
event,
|
|
60
|
+
filterByTk: 'admin',
|
|
61
|
+
collectionName: 'roles',
|
|
62
|
+
associationName: null,
|
|
63
|
+
sourceId: null,
|
|
64
|
+
},
|
|
65
|
+
{ debounce: true },
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('keeps using the association resource when the source record has an id', () => {
|
|
70
|
+
const { model, dispatchEvent } = createRolesFieldModel({ id: 1 });
|
|
71
|
+
const event = { type: 'click' };
|
|
72
|
+
|
|
73
|
+
model.onClick(event, { name: 'admin', title: 'Admin' });
|
|
74
|
+
|
|
75
|
+
expect(dispatchEvent).toHaveBeenCalledWith(
|
|
76
|
+
'click',
|
|
77
|
+
{
|
|
78
|
+
event,
|
|
79
|
+
filterByTk: 'admin',
|
|
80
|
+
collectionName: 'users',
|
|
81
|
+
associationName: 'users.roles',
|
|
82
|
+
sourceId: 1,
|
|
83
|
+
},
|
|
84
|
+
{ debounce: true },
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|