@nocobase/client-v2 2.2.0-alpha.1 → 2.2.0-alpha.2
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/PluginSettingsManager.d.ts +33 -0
- package/es/RouteRepository.d.ts +21 -6
- package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
- package/es/components/category-tabs/index.d.ts +9 -0
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/TypedVariableInput.d.ts +22 -4
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
- package/es/components/form/filter/index.d.ts +2 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +5 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +233 -137
- package/lib/index.js +234 -138
- package/package.json +7 -7
- package/src/PluginSettingsManager.ts +53 -0
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/PluginSettingsManager.test.ts +13 -0
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/browserChecker.test.ts +61 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +199 -0
- package/src/__tests__/plugin-manager.test.tsx +44 -2
- package/src/__tests__/settings-center.test.tsx +40 -1
- package/src/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- package/src/components/AppComponents.tsx +19 -3
- package/src/components/README.md +7 -1
- package/src/components/README.zh-CN.md +6 -1
- package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
- package/src/components/category-tabs/index.ts +10 -0
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/TypedVariableInput.tsx +416 -93
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +266 -9
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
- package/src/components/form/filter/index.ts +2 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +2 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +104 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/dataScopeFilter.ts +10 -0
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/linkageRules.tsx +123 -35
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/actions/openView.tsx +38 -4
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/BaseLayoutModel.tsx +25 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +5 -2
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +13 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +34 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +23 -9
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +82 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
- package/src/flow/components/FieldAssignValueInput.tsx +10 -5
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
- package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/components/fieldAssignOptions.ts +155 -27
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/getViewDiffAndUpdateHidden.tsx +1 -0
- package/src/flow/index.ts +2 -1
- package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +36 -13
- package/src/flow/models/blocks/table/TableBlockModel.tsx +38 -17
- package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +111 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
- package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
- package/src/flow/models/fields/InputFieldModel.tsx +40 -2
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +117 -1
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow/resolveViewParamsToViewList.tsx +11 -3
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/nocobase-buildin-plugin/index.tsx +44 -14
- package/src/settings-center/AdminSettingsLayout.tsx +10 -2
- package/src/settings-center/SystemSettingsPage.tsx +1 -2
- package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
- package/src/settings-center/plugin-manager/index.tsx +3 -0
- package/src/settings-center/utils.tsx +0 -6
|
@@ -28,7 +28,7 @@ import { buildFieldAssignCascaderOptionsFromCollection, type FieldAssignCascader
|
|
|
28
28
|
import { isRunJSValue, isVariableExpression, type MetaTreeNode } from '@nocobase/flow-engine';
|
|
29
29
|
import { isToManyAssociationField } from '../internal/utils/modelUtils';
|
|
30
30
|
|
|
31
|
-
export type AssignMode = 'default' | 'assign';
|
|
31
|
+
export type AssignMode = 'default' | 'assign' | 'override';
|
|
32
32
|
|
|
33
33
|
type CollectionFieldLike = {
|
|
34
34
|
name?: unknown;
|
|
@@ -89,6 +89,8 @@ export interface FieldAssignRulesEditorProps {
|
|
|
89
89
|
defaultMode?: AssignMode;
|
|
90
90
|
/** 固定 mode:用于“仅默认值/仅赋值”的场景 */
|
|
91
91
|
fixedMode?: AssignMode;
|
|
92
|
+
/** 可选 mode 列表;fixedMode 优先级更高 */
|
|
93
|
+
allowedModes?: AssignMode[];
|
|
92
94
|
/** 是否显示 condition */
|
|
93
95
|
showCondition?: boolean;
|
|
94
96
|
/** 是否显示 enable 开关 */
|
|
@@ -109,6 +111,65 @@ export interface FieldAssignRulesEditorProps {
|
|
|
109
111
|
maxAssociationFieldDepth?: number;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
const ALL_ASSIGN_MODES: AssignMode[] = ['default', 'override', 'assign'];
|
|
115
|
+
|
|
116
|
+
function cloneCascaderOption(option: FieldAssignCascaderOption): FieldAssignCascaderOption {
|
|
117
|
+
return {
|
|
118
|
+
...option,
|
|
119
|
+
children: option.children?.map((child) => cloneCascaderOption(child)),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mergeCascaderOptionInto(options: FieldAssignCascaderOption[], source: FieldAssignCascaderOption) {
|
|
124
|
+
const value = source?.value ? String(source.value) : '';
|
|
125
|
+
if (!value) return;
|
|
126
|
+
|
|
127
|
+
const existing = options.find((item) => String(item?.value || '') === value);
|
|
128
|
+
if (!existing) {
|
|
129
|
+
options.push(cloneCascaderOption(source));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!existing.label && source.label) {
|
|
134
|
+
existing.label = source.label;
|
|
135
|
+
}
|
|
136
|
+
if (source.isLeaf === false || existing.children?.length || source.children?.length) {
|
|
137
|
+
existing.isLeaf = false;
|
|
138
|
+
}
|
|
139
|
+
if (source.loading) {
|
|
140
|
+
existing.loading = source.loading;
|
|
141
|
+
}
|
|
142
|
+
if (source.children?.length) {
|
|
143
|
+
existing.children = mergeCascaderOptions(existing.children || [], source.children);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mergeCascaderOptions(
|
|
148
|
+
configured: FieldAssignCascaderOption[],
|
|
149
|
+
allFields: FieldAssignCascaderOption[],
|
|
150
|
+
): FieldAssignCascaderOption[] {
|
|
151
|
+
const result = (Array.isArray(configured) ? configured : []).map((item) => cloneCascaderOption(item));
|
|
152
|
+
for (const item of Array.isArray(allFields) ? allFields : []) {
|
|
153
|
+
mergeCascaderOptionInto(result, item);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isAssignMode(mode: unknown): mode is AssignMode {
|
|
159
|
+
return mode === 'default' || mode === 'override' || mode === 'assign';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeAssignModes(modes?: AssignMode[]): AssignMode[] {
|
|
163
|
+
const source = Array.isArray(modes) && modes.length ? modes : ALL_ASSIGN_MODES;
|
|
164
|
+
const out: AssignMode[] = [];
|
|
165
|
+
for (const mode of source) {
|
|
166
|
+
if (!isAssignMode(mode)) continue;
|
|
167
|
+
if (out.includes(mode)) continue;
|
|
168
|
+
out.push(mode);
|
|
169
|
+
}
|
|
170
|
+
return out.length ? out : ALL_ASSIGN_MODES;
|
|
171
|
+
}
|
|
172
|
+
|
|
112
173
|
export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (props) => {
|
|
113
174
|
const {
|
|
114
175
|
t,
|
|
@@ -118,6 +179,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
118
179
|
onChange,
|
|
119
180
|
defaultMode = 'assign',
|
|
120
181
|
fixedMode,
|
|
182
|
+
allowedModes: rawAllowedModes,
|
|
121
183
|
showCondition = true,
|
|
122
184
|
showEnable = true,
|
|
123
185
|
showValueEditorWhenNoField = false,
|
|
@@ -129,13 +191,43 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
129
191
|
} = props;
|
|
130
192
|
|
|
131
193
|
const value = React.useMemo(() => (Array.isArray(rawValue) ? rawValue : []), [rawValue]);
|
|
194
|
+
const allowedModes = React.useMemo(
|
|
195
|
+
() => normalizeAssignModes(fixedMode ? [fixedMode] : rawAllowedModes),
|
|
196
|
+
[fixedMode, rawAllowedModes],
|
|
197
|
+
);
|
|
198
|
+
const getDefaultMode = React.useCallback((): AssignMode => {
|
|
199
|
+
if (fixedMode) return fixedMode;
|
|
200
|
+
if (isAssignMode(defaultMode) && allowedModes.includes(defaultMode)) return defaultMode;
|
|
201
|
+
return allowedModes[0] || 'assign';
|
|
202
|
+
}, [allowedModes, defaultMode, fixedMode]);
|
|
203
|
+
const normalizeItemMode = React.useCallback(
|
|
204
|
+
(item: FieldAssignRuleItem): FieldAssignRuleItem => {
|
|
205
|
+
const mode = isAssignMode(item?.mode) && allowedModes.includes(item.mode) ? item.mode : getDefaultMode();
|
|
206
|
+
return item?.mode === mode ? item : { ...item, mode };
|
|
207
|
+
},
|
|
208
|
+
[allowedModes, getDefaultMode],
|
|
209
|
+
);
|
|
210
|
+
const emitChange = React.useCallback(
|
|
211
|
+
(next: FieldAssignRuleItem[]) => {
|
|
212
|
+
onChange?.(next.map((item) => normalizeItemMode(item)));
|
|
213
|
+
},
|
|
214
|
+
[normalizeItemMode, onChange],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const normalizeFieldOptions = React.useCallback((options: FieldAssignRulesEditorProps['fieldOptions']) => {
|
|
218
|
+
return Array.isArray(options)
|
|
219
|
+
? (options as FieldAssignCascaderOption[]).map((option) => cloneCascaderOption(option))
|
|
220
|
+
: [];
|
|
221
|
+
}, []);
|
|
132
222
|
const [cascaderOptions, setCascaderOptions] = React.useState<FieldAssignCascaderOption[]>(() =>
|
|
133
|
-
|
|
223
|
+
normalizeFieldOptions(fieldOptions),
|
|
134
224
|
);
|
|
225
|
+
const loadedCascaderOptionsRef = React.useRef<WeakSet<FieldAssignCascaderOption>>(new WeakSet());
|
|
135
226
|
|
|
136
227
|
React.useEffect(() => {
|
|
137
|
-
|
|
138
|
-
|
|
228
|
+
loadedCascaderOptionsRef.current = new WeakSet();
|
|
229
|
+
setCascaderOptions(normalizeFieldOptions(fieldOptions));
|
|
230
|
+
}, [fieldOptions, normalizeFieldOptions, rootCollection, maxAssociationFieldDepth]);
|
|
139
231
|
|
|
140
232
|
const getRuleKey = React.useCallback((item: FieldAssignRuleItem, index: number) => item?.key || String(index), []);
|
|
141
233
|
const [titleFieldDraftMap, setTitleFieldDraftMap] = React.useState<Record<string, string | undefined>>({});
|
|
@@ -312,7 +404,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
312
404
|
|
|
313
405
|
const patchItem = (index: number, patch: Partial<FieldAssignRuleItem>) => {
|
|
314
406
|
const next = value.map((it, i) => (i === index ? { ...it, ...patch } : it));
|
|
315
|
-
|
|
407
|
+
emitChange(next);
|
|
316
408
|
};
|
|
317
409
|
|
|
318
410
|
const removeItem = (index: number) => {
|
|
@@ -334,7 +426,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
334
426
|
setSyncingRuleKey(null);
|
|
335
427
|
}
|
|
336
428
|
const next = value.filter((_, i) => i !== index);
|
|
337
|
-
|
|
429
|
+
emitChange(next);
|
|
338
430
|
};
|
|
339
431
|
|
|
340
432
|
const moveItem = (index: number, direction: 'up' | 'down') => {
|
|
@@ -343,7 +435,7 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
343
435
|
if (targetIndex < 0 || targetIndex >= next.length) return;
|
|
344
436
|
const [item] = next.splice(index, 1);
|
|
345
437
|
next.splice(targetIndex, 0, item);
|
|
346
|
-
|
|
438
|
+
emitChange(next);
|
|
347
439
|
};
|
|
348
440
|
|
|
349
441
|
const addItem = () => {
|
|
@@ -352,29 +444,33 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
352
444
|
{
|
|
353
445
|
key: uid(),
|
|
354
446
|
enable: true,
|
|
355
|
-
mode:
|
|
447
|
+
mode: getDefaultMode(),
|
|
356
448
|
condition: { logic: '$and', items: [] },
|
|
357
449
|
targetPath: undefined,
|
|
358
450
|
value: undefined,
|
|
359
451
|
},
|
|
360
452
|
];
|
|
361
|
-
|
|
453
|
+
emitChange(next);
|
|
362
454
|
};
|
|
363
455
|
|
|
364
456
|
const getEffectiveMode = (item: FieldAssignRuleItem): AssignMode => {
|
|
365
457
|
if (fixedMode) return fixedMode;
|
|
366
|
-
|
|
458
|
+
if (isAssignMode(item?.mode) && allowedModes.includes(item.mode)) return item.mode;
|
|
459
|
+
return getDefaultMode();
|
|
367
460
|
};
|
|
368
461
|
|
|
369
462
|
const renderAssignModeLabel = React.useCallback(
|
|
370
463
|
(mode: AssignMode) => {
|
|
371
|
-
const modeText =
|
|
464
|
+
const modeText =
|
|
465
|
+
mode === 'default' ? t('Default value') : mode === 'override' ? t('Override value') : t('Fixed value');
|
|
372
466
|
const modeHelpText =
|
|
373
467
|
mode === 'default'
|
|
374
468
|
? t(
|
|
375
469
|
'A pre-filled value. Editable, for new entries only, and won’t affect existing data (including empty values).',
|
|
376
470
|
)
|
|
377
|
-
:
|
|
471
|
+
: mode === 'override'
|
|
472
|
+
? t('Overrides existing values until the user edits it.')
|
|
473
|
+
: t('A system-set value. Read-only.');
|
|
378
474
|
|
|
379
475
|
const preventModeToggle = (e: React.MouseEvent<HTMLElement>) => {
|
|
380
476
|
e.preventDefault();
|
|
@@ -551,23 +647,26 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
551
647
|
const opts = selectedOptions || [];
|
|
552
648
|
const target = opts[opts.length - 1];
|
|
553
649
|
if (!target) return;
|
|
554
|
-
if (target.children && Array.isArray(target.children) && target.children.length) return;
|
|
555
650
|
if (target.isLeaf) return;
|
|
651
|
+
if (loadedCascaderOptionsRef.current.has(target)) return;
|
|
556
652
|
|
|
557
653
|
const segments = opts.map((o) => String(o?.value)).filter(Boolean);
|
|
558
654
|
const targetCollection = resolveTargetCollectionBySegments(segments);
|
|
559
655
|
if (!targetCollection) {
|
|
560
|
-
target.isLeaf =
|
|
656
|
+
target.isLeaf = !(target.children && target.children.length);
|
|
657
|
+
loadedCascaderOptionsRef.current.add(target);
|
|
561
658
|
setCascaderOptions((prev) => [...prev]);
|
|
562
659
|
return;
|
|
563
660
|
}
|
|
564
661
|
|
|
565
662
|
const children = buildChildrenFromCollection(targetCollection, segments.length);
|
|
566
663
|
if (!children.length) {
|
|
567
|
-
target.isLeaf =
|
|
664
|
+
target.isLeaf = !(target.children && target.children.length);
|
|
568
665
|
} else {
|
|
569
|
-
target.children = children;
|
|
666
|
+
target.children = mergeCascaderOptions(target.children || [], children);
|
|
667
|
+
target.isLeaf = false;
|
|
570
668
|
}
|
|
669
|
+
loadedCascaderOptionsRef.current.add(target);
|
|
571
670
|
setCascaderOptions((prev) => [...prev]);
|
|
572
671
|
},
|
|
573
672
|
[buildChildrenFromCollection, resolveTargetCollectionBySegments],
|
|
@@ -583,12 +682,14 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
583
682
|
const hit = options.find((o) => String(o?.value) === seg);
|
|
584
683
|
if (!hit) return;
|
|
585
684
|
selected.push(hit);
|
|
685
|
+
if (!hit.isLeaf && !loadedCascaderOptionsRef.current.has(hit)) {
|
|
686
|
+
await loadCascaderData(selected);
|
|
687
|
+
}
|
|
586
688
|
if (hit.children?.length) {
|
|
587
689
|
options = hit.children;
|
|
588
690
|
continue;
|
|
589
691
|
}
|
|
590
692
|
if (hit.isLeaf) return;
|
|
591
|
-
await loadCascaderData(selected);
|
|
592
693
|
options = hit.children || [];
|
|
593
694
|
}
|
|
594
695
|
},
|
|
@@ -619,7 +720,9 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
619
720
|
await preloadCascaderPath(segs);
|
|
620
721
|
}
|
|
621
722
|
};
|
|
622
|
-
|
|
723
|
+
run().catch((error) => {
|
|
724
|
+
console.warn('[FieldAssignRulesEditor] Failed to preload cascader path', error);
|
|
725
|
+
});
|
|
623
726
|
return () => {
|
|
624
727
|
cancelled = true;
|
|
625
728
|
};
|
|
@@ -627,7 +730,8 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
627
730
|
|
|
628
731
|
const renderPanelHeader = (item: FieldAssignRuleItem, index: number) => {
|
|
629
732
|
const mode = getEffectiveMode(item);
|
|
630
|
-
const modeText =
|
|
733
|
+
const modeText =
|
|
734
|
+
mode === 'default' ? t('Default value') : mode === 'override' ? t('Override value') : t('Fixed value');
|
|
631
735
|
const fieldLabel = getFieldLabel(item.targetPath);
|
|
632
736
|
const title = fieldLabel ? String(fieldLabel) : t('Please select field');
|
|
633
737
|
|
|
@@ -935,8 +1039,11 @@ export const FieldAssignRulesEditor: React.FC<FieldAssignRulesEditorProps> = (pr
|
|
|
935
1039
|
onChange={(event) => patchItem(index, { mode: event.target.value as AssignMode })}
|
|
936
1040
|
>
|
|
937
1041
|
<Space size={16}>
|
|
938
|
-
|
|
939
|
-
|
|
1042
|
+
{allowedModes.map((allowedMode) => (
|
|
1043
|
+
<Radio key={allowedMode} value={allowedMode}>
|
|
1044
|
+
{renderAssignModeLabel(allowedMode)}
|
|
1045
|
+
</Radio>
|
|
1046
|
+
))}
|
|
940
1047
|
</Space>
|
|
941
1048
|
</Radio.Group>
|
|
942
1049
|
</div>
|
|
@@ -341,6 +341,8 @@ interface Props {
|
|
|
341
341
|
* 默认 false,保持历史行为。
|
|
342
342
|
*/
|
|
343
343
|
enableDateVariableAsConstant?: boolean;
|
|
344
|
+
/** 是否允许在变量选择器中使用 RunJS。默认 true,保持历史行为。 */
|
|
345
|
+
allowRunJS?: boolean;
|
|
344
346
|
maxAssociationFieldDepth?: number;
|
|
345
347
|
}
|
|
346
348
|
|
|
@@ -712,6 +714,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
712
714
|
preferFormItemFieldModel,
|
|
713
715
|
associationFieldNamesOverride,
|
|
714
716
|
enableDateVariableAsConstant = false,
|
|
717
|
+
allowRunJS = true,
|
|
715
718
|
maxAssociationFieldDepth = 2,
|
|
716
719
|
}) => {
|
|
717
720
|
const flowCtx = useFlowContext<FlowModelContext>();
|
|
@@ -1417,11 +1420,13 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1417
1420
|
render: ConstantEditor,
|
|
1418
1421
|
},
|
|
1419
1422
|
{ title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
1420
|
-
|
|
1423
|
+
...(allowRunJS
|
|
1424
|
+
? [{ title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent }]
|
|
1425
|
+
: []),
|
|
1421
1426
|
...limitedBase,
|
|
1422
1427
|
];
|
|
1423
1428
|
};
|
|
1424
|
-
}, [flowCtx, ConstantEditor, NullComponent, RunJSComponent, maxAssociationFieldDepth]);
|
|
1429
|
+
}, [flowCtx, ConstantEditor, NullComponent, RunJSComponent, allowRunJS, maxAssociationFieldDepth]);
|
|
1425
1430
|
|
|
1426
1431
|
const displayValue = React.useMemo(() => {
|
|
1427
1432
|
if (!useDateVariableConstant) {
|
|
@@ -1465,7 +1470,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1465
1470
|
const firstPath = meta?.paths?.[0];
|
|
1466
1471
|
if (firstPath === 'constant') return ConstantEditor;
|
|
1467
1472
|
if (firstPath === 'null') return NullComponent;
|
|
1468
|
-
if (firstPath === 'runjs') return RunJSComponent;
|
|
1473
|
+
if (allowRunJS && firstPath === 'runjs') return RunJSComponent;
|
|
1469
1474
|
return null;
|
|
1470
1475
|
},
|
|
1471
1476
|
resolveValueFromPath: (item) => {
|
|
@@ -1474,12 +1479,12 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1474
1479
|
return useDateVariableConstant ? { type: 'today' } : '';
|
|
1475
1480
|
}
|
|
1476
1481
|
if (firstPath === 'null') return null;
|
|
1477
|
-
if (firstPath === 'runjs') return { code: '', version: 'v2' };
|
|
1482
|
+
if (allowRunJS && firstPath === 'runjs') return { code: '', version: 'v2' };
|
|
1478
1483
|
return undefined;
|
|
1479
1484
|
},
|
|
1480
1485
|
resolvePathFromValue: (currentValue) => {
|
|
1481
1486
|
if (currentValue === null) return ['null'];
|
|
1482
|
-
if (isRunJSValue(currentValue)) return ['runjs'];
|
|
1487
|
+
if (allowRunJS && isRunJSValue(currentValue)) return ['runjs'];
|
|
1483
1488
|
if (useDateVariableConstant && isCtxDateExpression(currentValue)) {
|
|
1484
1489
|
return ['constant'];
|
|
1485
1490
|
}
|
|
@@ -28,6 +28,13 @@ type FlowRouteGuardState = {
|
|
|
28
28
|
|
|
29
29
|
export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
|
|
30
30
|
|
|
31
|
+
type FlowRouteLayoutContext = {
|
|
32
|
+
authCheck?: boolean;
|
|
33
|
+
routePath?: string;
|
|
34
|
+
routeName?: string;
|
|
35
|
+
uid?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
31
38
|
export type FlowRouteProps = {
|
|
32
39
|
pageUid?: string;
|
|
33
40
|
active?: boolean;
|
|
@@ -38,7 +45,7 @@ export type FlowRouteProps = {
|
|
|
38
45
|
const getDefaultAdminLayoutModel = (flowEngine: FlowEngine) =>
|
|
39
46
|
getAdminLayoutModel<AdminLayoutModel>(flowEngine, { required: true });
|
|
40
47
|
|
|
41
|
-
const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?:
|
|
48
|
+
const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: FlowRouteLayoutContext) => {
|
|
42
49
|
const layout = contextLayout || flowEngine.context.layout;
|
|
43
50
|
|
|
44
51
|
if (layout?.uid) {
|
|
@@ -48,7 +55,10 @@ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
|
|
|
48
55
|
return getDefaultAdminLayoutModel(flowEngine);
|
|
49
56
|
};
|
|
50
57
|
|
|
51
|
-
const getDefaultLegacyPageBehavior = (
|
|
58
|
+
const getDefaultLegacyPageBehavior = (
|
|
59
|
+
flowEngine: FlowEngine,
|
|
60
|
+
contextLayout?: FlowRouteLayoutContext,
|
|
61
|
+
): LegacyPageBehavior => {
|
|
52
62
|
const layout = contextLayout || flowEngine.context.layout;
|
|
53
63
|
|
|
54
64
|
if (layout?.routeName && layout.routeName !== 'admin') {
|
|
@@ -58,6 +68,8 @@ const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: an
|
|
|
58
68
|
return 'redirect';
|
|
59
69
|
};
|
|
60
70
|
|
|
71
|
+
const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
|
|
72
|
+
|
|
61
73
|
const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
|
|
62
74
|
if (flowEngine.getModel(pageUid)) {
|
|
63
75
|
return true;
|
|
@@ -148,18 +160,46 @@ const BridgeFlowRoute = ({
|
|
|
148
160
|
* @throws {Error} 当缺少 `route.params.name` 时抛出异常
|
|
149
161
|
*/
|
|
150
162
|
const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
163
|
+
const {
|
|
164
|
+
active,
|
|
165
|
+
getLayoutModel: getLayoutModelProp,
|
|
166
|
+
legacyPageBehavior: legacyPageBehaviorProp,
|
|
167
|
+
pageUid: pageUidProp,
|
|
168
|
+
} = props;
|
|
151
169
|
const flowEngine = useFlowEngine();
|
|
152
|
-
const flowContext = useFlowContext<
|
|
170
|
+
const flowContext = useFlowContext<{ layout?: FlowRouteLayoutContext }>();
|
|
153
171
|
const contextLayout = flowContext?.layout;
|
|
172
|
+
const propsLayoutModel = useMemo(() => getLayoutModelProp?.(flowEngine), [flowEngine, getLayoutModelProp]);
|
|
173
|
+
const rawRouteLayout = contextLayout || propsLayoutModel?.layout;
|
|
174
|
+
const routeLayoutUid = rawRouteLayout?.uid;
|
|
175
|
+
const routeLayoutRouteName = rawRouteLayout?.routeName;
|
|
176
|
+
const routeLayoutRoutePath = rawRouteLayout?.routePath;
|
|
177
|
+
const routeLayoutAuthCheck = rawRouteLayout?.authCheck;
|
|
178
|
+
const routeLayout = useMemo(() => {
|
|
179
|
+
if (
|
|
180
|
+
!routeLayoutUid &&
|
|
181
|
+
!routeLayoutRouteName &&
|
|
182
|
+
!routeLayoutRoutePath &&
|
|
183
|
+
typeof routeLayoutAuthCheck === 'undefined'
|
|
184
|
+
) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
authCheck: routeLayoutAuthCheck,
|
|
189
|
+
routeName: routeLayoutRouteName,
|
|
190
|
+
routePath: routeLayoutRoutePath,
|
|
191
|
+
uid: routeLayoutUid,
|
|
192
|
+
};
|
|
193
|
+
}, [routeLayoutAuthCheck, routeLayoutRouteName, routeLayoutRoutePath, routeLayoutUid]);
|
|
154
194
|
const getLayoutModel = useMemo(
|
|
155
|
-
() =>
|
|
156
|
-
[
|
|
195
|
+
() => getLayoutModelProp || ((engine: FlowEngine) => getDefaultLayoutModel(engine, routeLayout)),
|
|
196
|
+
[getLayoutModelProp, routeLayout],
|
|
157
197
|
);
|
|
158
|
-
const legacyPageBehavior =
|
|
198
|
+
const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
|
|
159
199
|
const app = useApp();
|
|
160
200
|
const routeRepository = flowEngine.context.routeRepository;
|
|
161
201
|
const params = useParams();
|
|
162
|
-
const pageUid =
|
|
202
|
+
const pageUid = pageUidProp || params?.name;
|
|
163
203
|
const skipRouteRepositoryCheck = !routeRepository;
|
|
164
204
|
const [guardState, setGuardState] = useState<FlowRouteGuardState>({
|
|
165
205
|
pending: true,
|
|
@@ -196,6 +236,11 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
|
|
239
|
+
if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
|
|
240
|
+
setGuardState({ pending: false, allowBridge: false, notFound: true });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
199
244
|
if (!route && legacyPageBehavior === 'notFound') {
|
|
200
245
|
const flowModelExists = await hasFlowModel(flowEngine, pageUid);
|
|
201
246
|
if (active && requestId === requestIdRef.current) {
|
|
@@ -245,12 +290,12 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
245
290
|
}
|
|
246
291
|
};
|
|
247
292
|
|
|
248
|
-
|
|
293
|
+
run();
|
|
249
294
|
|
|
250
295
|
return () => {
|
|
251
296
|
active = false;
|
|
252
297
|
};
|
|
253
|
-
}, [app, flowEngine, legacyPageBehavior, pageUid, routeRepository, skipRouteRepositoryCheck]);
|
|
298
|
+
}, [app, flowEngine, legacyPageBehavior, pageUid, routeLayout, routeRepository, skipRouteRepositoryCheck]);
|
|
254
299
|
|
|
255
300
|
const content = useMemo(() => {
|
|
256
301
|
if (guardState.pending) {
|
|
@@ -265,8 +310,8 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
265
310
|
return null;
|
|
266
311
|
}
|
|
267
312
|
|
|
268
|
-
return <BridgeFlowRoute pageUid={pageUid} active={
|
|
269
|
-
}, [getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid
|
|
313
|
+
return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
|
|
314
|
+
}, [active, getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
|
|
270
315
|
|
|
271
316
|
return content;
|
|
272
317
|
};
|
|
@@ -10,19 +10,24 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
|
13
|
-
import { ActionScene, FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID } from '@nocobase/flow-engine';
|
|
13
|
+
import { ActionScene, FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID, useFlowContext } from '@nocobase/flow-engine';
|
|
14
14
|
import { DynamicFlowsIcon } from '../DynamicFlowsIcon';
|
|
15
15
|
|
|
16
16
|
const mockState = vi.hoisted(() => ({
|
|
17
17
|
capturedSelectProps: [] as any[],
|
|
18
|
+
capturedTabsProps: [] as any[],
|
|
18
19
|
flowContextValue: undefined as any,
|
|
19
20
|
}));
|
|
20
21
|
|
|
21
22
|
vi.mock('@nocobase/flow-engine', async (importOriginal) => {
|
|
22
23
|
const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
|
|
24
|
+
const React = await import('react');
|
|
23
25
|
return {
|
|
24
26
|
...actual,
|
|
25
|
-
useFlowContext: () =>
|
|
27
|
+
useFlowContext: () => {
|
|
28
|
+
const context = React.useContext(actual.FlowReactContext);
|
|
29
|
+
return context?.view ? context : mockState.flowContextValue;
|
|
30
|
+
},
|
|
26
31
|
};
|
|
27
32
|
});
|
|
28
33
|
|
|
@@ -62,6 +67,33 @@ vi.mock('antd', async (importOriginal) => {
|
|
|
62
67
|
return <div data-testid={String(props.placeholder || 'select')} />;
|
|
63
68
|
},
|
|
64
69
|
Space: ({ children }: any) => <div>{children}</div>,
|
|
70
|
+
Tabs: (props: any) => {
|
|
71
|
+
mockState.capturedTabsProps.push(props);
|
|
72
|
+
const { items, activeKey, onChange, tabBarStyle } = props;
|
|
73
|
+
const currentKey = activeKey || items?.[0]?.key;
|
|
74
|
+
return (
|
|
75
|
+
<div data-testid="dynamic-flow-source-tabs">
|
|
76
|
+
<div role="tablist" style={tabBarStyle}>
|
|
77
|
+
{items?.map((item: any) => (
|
|
78
|
+
<button
|
|
79
|
+
key={item.key}
|
|
80
|
+
role="tab"
|
|
81
|
+
aria-selected={item.key === currentKey}
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => onChange?.(item.key)}
|
|
84
|
+
>
|
|
85
|
+
{item.label}
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
{items?.map((item: any) => (
|
|
90
|
+
<div key={item.key} hidden={item.key !== currentKey}>
|
|
91
|
+
{item.children}
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
65
97
|
Tooltip: ({ children }: any) => <>{children}</>,
|
|
66
98
|
};
|
|
67
99
|
});
|
|
@@ -83,6 +115,8 @@ const openDynamicFlowsEditor = async (model: FlowModel) => {
|
|
|
83
115
|
expect(trigger).not.toBeNull();
|
|
84
116
|
await userEvent.click(trigger as Element);
|
|
85
117
|
|
|
118
|
+
await waitFor(() => expect(model.context.viewer.embed).toHaveBeenCalled());
|
|
119
|
+
|
|
86
120
|
const embedCall = (model.context.viewer.embed as any).mock.calls.at(-1)?.[0];
|
|
87
121
|
expect(embedCall?.content).toBeTruthy();
|
|
88
122
|
|
|
@@ -163,6 +197,38 @@ const createModel = (options: { preventClose?: boolean; hiddenClose?: boolean; s
|
|
|
163
197
|
return model;
|
|
164
198
|
};
|
|
165
199
|
|
|
200
|
+
const createPeerModel = (model: FlowModel, uid: string, saveStepParams = vi.fn(async () => undefined)) => {
|
|
201
|
+
const PeerModel = model.constructor as typeof FlowModel;
|
|
202
|
+
const peer = new PeerModel({
|
|
203
|
+
uid,
|
|
204
|
+
use: PeerModel,
|
|
205
|
+
flowEngine: model.flowEngine,
|
|
206
|
+
stepParams: {},
|
|
207
|
+
subModels: {},
|
|
208
|
+
flowRegistry: {
|
|
209
|
+
flow1: {
|
|
210
|
+
title: 'Template event flow',
|
|
211
|
+
on: 'click',
|
|
212
|
+
steps: {
|
|
213
|
+
notifyStep: {
|
|
214
|
+
use: 'notify',
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
} as any);
|
|
220
|
+
|
|
221
|
+
peer.context.defineProperty('message', {
|
|
222
|
+
value: {
|
|
223
|
+
error: vi.fn(),
|
|
224
|
+
success: vi.fn(),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
vi.spyOn(peer, 'saveStepParams').mockImplementation(saveStepParams);
|
|
228
|
+
|
|
229
|
+
return peer;
|
|
230
|
+
};
|
|
231
|
+
|
|
166
232
|
const createView = () => {
|
|
167
233
|
let destroyed = false;
|
|
168
234
|
let closingPromise: Promise<boolean | void> | undefined;
|
|
@@ -189,6 +255,9 @@ const createView = () => {
|
|
|
189
255
|
destroyed = true;
|
|
190
256
|
}),
|
|
191
257
|
beforeClose: undefined as any,
|
|
258
|
+
inputArgs: {
|
|
259
|
+
pageActive: true,
|
|
260
|
+
},
|
|
192
261
|
};
|
|
193
262
|
return view;
|
|
194
263
|
};
|
|
@@ -196,6 +265,7 @@ const createView = () => {
|
|
|
196
265
|
describe('DynamicFlowsIcon', () => {
|
|
197
266
|
beforeEach(() => {
|
|
198
267
|
mockState.capturedSelectProps.length = 0;
|
|
268
|
+
mockState.capturedTabsProps.length = 0;
|
|
199
269
|
mockState.flowContextValue = {
|
|
200
270
|
modal: {
|
|
201
271
|
confirm: vi.fn(),
|
|
@@ -377,4 +447,80 @@ describe('DynamicFlowsIcon', () => {
|
|
|
377
447
|
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
378
448
|
expect(model.context.message.success).toHaveBeenCalledWith('Configuration saved');
|
|
379
449
|
});
|
|
450
|
+
|
|
451
|
+
it('renders source tabs and closes the editor when saving one source', async () => {
|
|
452
|
+
const model = createModel();
|
|
453
|
+
const targetSaveStepParams = vi.fn(async () => undefined);
|
|
454
|
+
const target = createPeerModel(model, 'target-model', targetSaveStepParams);
|
|
455
|
+
|
|
456
|
+
model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
|
|
457
|
+
key: 'test-source-provider',
|
|
458
|
+
getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
462
|
+
|
|
463
|
+
expect(screen.getByRole('tab', { name: 'Current block' })).toHaveAttribute('aria-selected', 'true');
|
|
464
|
+
expect(mockState.capturedTabsProps.at(-1)?.tabBarStyle?.paddingLeft).toBeGreaterThan(0);
|
|
465
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
|
|
466
|
+
|
|
467
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
468
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
469
|
+
|
|
470
|
+
expect(targetSaveStepParams).toHaveBeenCalledTimes(1);
|
|
471
|
+
expect(target.flowRegistry.getFlows().size).toBe(2);
|
|
472
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
473
|
+
expect(target.context.message.success).toHaveBeenCalledWith('Configuration saved');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('saves all dirty sources without discard confirmation', async () => {
|
|
477
|
+
const saveStepParams = vi.fn(async () => undefined);
|
|
478
|
+
const model = createModel({ saveStepParams });
|
|
479
|
+
const targetSaveStepParams = vi.fn(async () => undefined);
|
|
480
|
+
const target = createPeerModel(model, 'target-model', targetSaveStepParams);
|
|
481
|
+
mockState.flowContextValue.modal.confirm.mockResolvedValue(false);
|
|
482
|
+
|
|
483
|
+
model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
|
|
484
|
+
key: 'test-source-unsaved-provider',
|
|
485
|
+
getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
489
|
+
|
|
490
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
|
|
491
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
492
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Current block' }));
|
|
493
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
494
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
495
|
+
|
|
496
|
+
expect(saveStepParams).toHaveBeenCalledTimes(1);
|
|
497
|
+
expect(targetSaveStepParams).toHaveBeenCalledTimes(1);
|
|
498
|
+
expect(mockState.flowContextValue.modal.confirm).not.toHaveBeenCalled();
|
|
499
|
+
expect(view.close).toHaveBeenCalledTimes(1);
|
|
500
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(model.flowRegistry.getFlows().size).toBe(2);
|
|
502
|
+
expect(target.flowRegistry.getFlows().size).toBe(2);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('provides the active source model context inside source tabs', async () => {
|
|
506
|
+
const model = createModel();
|
|
507
|
+
const target = createPeerModel(model, 'target-model');
|
|
508
|
+
const seenModelUids: string[] = [];
|
|
509
|
+
const ContextProbe = () => {
|
|
510
|
+
const ctx = useFlowContext<any>();
|
|
511
|
+
seenModelUids.push(ctx?.model?.uid);
|
|
512
|
+
return null;
|
|
513
|
+
};
|
|
514
|
+
model.flowEngine.flowSettings.renderStepForm = vi.fn(() => <ContextProbe />) as any;
|
|
515
|
+
|
|
516
|
+
model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
|
|
517
|
+
key: 'test-source-context-provider',
|
|
518
|
+
getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await openDynamicFlowsEditor(model);
|
|
522
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
|
|
523
|
+
|
|
524
|
+
await waitFor(() => expect(seenModelUids).toContain('target-model'));
|
|
525
|
+
});
|
|
380
526
|
});
|