@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.10
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/APIClient.d.ts +12 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/actions/linkageRules.d.ts +25 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
- package/es/flow/components/FlowRoute.d.ts +2 -2
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/ActionModelCore.d.ts +4 -2
- package/es/flow/models/base/BlockGridModel.d.ts +3 -0
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +148 -145
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +170 -167
- package/lib/locale/languageCodes.js +1 -0
- package/package.json +10 -8
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
- package/src/__tests__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
- package/src/collection-manager/field-configure.ts +2 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
- package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
- package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
- package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
- package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +59 -16
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
- package/src/flow/common/Markdown/style.ts +4 -0
- package/src/flow/components/BlockItemCard.tsx +2 -2
- package/src/flow/components/FieldAssignValueInput.tsx +42 -621
- package/src/flow/components/FlowRoute.tsx +71 -29
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/index.ts +2 -0
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/ActionModelCore.tsx +11 -2
- package/src/flow/models/base/BlockGridModel.tsx +26 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
- package/src/flow/models/base/GridModel.tsx +0 -1
- package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
- package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
- package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
- package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
- package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
- package/src/index.ts +1 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/locale/languageCodes.ts +1 -0
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -34,6 +34,8 @@ const { DateFilterDynamicComponent: DateFilterDynamicComponentLazy } = lazy(
|
|
|
34
34
|
'DateFilterDynamicComponent',
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
+
const fallbackInputComponents = new Set(['Input', 'Input.URL', 'NanoIDInput', 'DatePicker']);
|
|
38
|
+
|
|
37
39
|
// 简化的本地辅助:按需获取 ctx.collection 的字段树(MetaTreeNode[])
|
|
38
40
|
async function buildCollectionLeftMetaTreeLocal(ctx: any): Promise<MetaTreeNode[]> {
|
|
39
41
|
const resolve = async (sub: any): Promise<MetaTreeNode[]> => {
|
|
@@ -137,9 +139,16 @@ export interface VariableFilterItemProps {
|
|
|
137
139
|
rightVariableConverters?: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'>;
|
|
138
140
|
ignoreFieldNames?: string[];
|
|
139
141
|
maxAssociationFieldDepth?: number;
|
|
142
|
+
/**
|
|
143
|
+
* 右侧变量树的关联层级上限,默认与 `maxAssociationFieldDepth` 一致。
|
|
144
|
+
* 传 `null` 表示不限制:右侧是变量树而非集合字段树,其深度可能已由调用方约束
|
|
145
|
+
* (例如工作流的变量树由触发器的「预加载关系数据」决定),此时再按左侧的层级上限裁剪
|
|
146
|
+
* 会让已配置好的深层变量选不到。
|
|
147
|
+
*/
|
|
148
|
+
rightMaxAssociationFieldDepth?: number | null;
|
|
140
149
|
}
|
|
141
150
|
|
|
142
|
-
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number) {
|
|
151
|
+
function limitMetaTreeIfNeeded(nodes: MetaTreeNode[], maxAssociationFieldDepth?: number | null) {
|
|
143
152
|
if (typeof maxAssociationFieldDepth !== 'number') {
|
|
144
153
|
return nodes;
|
|
145
154
|
}
|
|
@@ -276,9 +285,9 @@ function createStaticInputRenderer(
|
|
|
276
285
|
onChange={(v: unknown) => onChange?.(v as VariableFilterItemValue['value'])}
|
|
277
286
|
/>
|
|
278
287
|
);
|
|
279
|
-
//
|
|
288
|
+
// 未直接处理的 x-component:尝试从 app 解析组件,可由筛选器接管的过渡组件查找失败时静默回退到 Input
|
|
280
289
|
if (xComp && app?.getComponent) {
|
|
281
|
-
const Comp = app.getComponent(xComp
|
|
290
|
+
const Comp = app.getComponent(xComp, !fallbackInputComponents.has(xComp));
|
|
282
291
|
if (Comp) {
|
|
283
292
|
return (
|
|
284
293
|
<Comp
|
|
@@ -365,6 +374,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
365
374
|
rightVariableConverters,
|
|
366
375
|
ignoreFieldNames,
|
|
367
376
|
maxAssociationFieldDepth,
|
|
377
|
+
rightMaxAssociationFieldDepth = maxAssociationFieldDepth,
|
|
368
378
|
}) => {
|
|
369
379
|
// 使用 View 上下文,确保可访问 ctx.view 的异步子树
|
|
370
380
|
const ctx = useFlowViewContext();
|
|
@@ -595,8 +605,14 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
595
605
|
|
|
596
606
|
const renderRightValueComponent = useCallback(() => {
|
|
597
607
|
// 文本类多关键词:优先使用操作符 schema 声明的组件
|
|
598
|
-
const resolved = resolveOperatorComponent(model.context.app, operator, operatorMetaList);
|
|
599
608
|
const supportKeyword = operator === '$in' || operator === '$notIn';
|
|
609
|
+
const operatorComponent = currentOpMeta?.schema?.['x-component'] as string | undefined;
|
|
610
|
+
const isLocallyRenderedComponent =
|
|
611
|
+
operatorComponent === 'Select' ||
|
|
612
|
+
(typeof operatorComponent === 'string' && fallbackInputComponents.has(operatorComponent));
|
|
613
|
+
const resolved = supportKeyword
|
|
614
|
+
? resolveOperatorComponent(model.context.app, operator, operatorMetaList, !isLocallyRenderedComponent)
|
|
615
|
+
: null;
|
|
600
616
|
if (resolved && supportKeyword) {
|
|
601
617
|
const { Comp, props: xProps } = resolved;
|
|
602
618
|
const nextProps = { ...xProps };
|
|
@@ -642,6 +658,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
642
658
|
disabled,
|
|
643
659
|
operator,
|
|
644
660
|
operatorMetaList,
|
|
661
|
+
currentOpMeta,
|
|
645
662
|
rightValue,
|
|
646
663
|
staticInputRenderer,
|
|
647
664
|
model.context.app,
|
|
@@ -676,10 +693,10 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
676
693
|
{ title: t('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent },
|
|
677
694
|
...nodes,
|
|
678
695
|
],
|
|
679
|
-
|
|
696
|
+
rightMaxAssociationFieldDepth,
|
|
680
697
|
);
|
|
681
698
|
};
|
|
682
|
-
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t,
|
|
699
|
+
}, [rightMetaTree, ctx, staticInputRenderer, NullComponent, t, rightMaxAssociationFieldDepth]);
|
|
683
700
|
|
|
684
701
|
// 当启用右侧变量输入时,构造 VariableInput 的 converters:
|
|
685
702
|
// - 变量模式:返回 null 让 VariableInput 渲染 VariableTag
|
|
@@ -0,0 +1,158 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
13
|
+
import type { MetaTreeNode } from '@nocobase/flow-engine';
|
|
14
|
+
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
15
|
+
import { VariableFilterItem } from '../VariableFilterItem';
|
|
16
|
+
import { createMockFlowApp, TestCollectionFieldInterface } from '../../../__tests__/helpers/mockFlowApp';
|
|
17
|
+
|
|
18
|
+
const captured = vi.hoisted(() => ({ metaTrees: [] as any[] }));
|
|
19
|
+
|
|
20
|
+
vi.mock('@nocobase/flow-engine', async () => {
|
|
21
|
+
const actual = await vi.importActual<any>('@nocobase/flow-engine');
|
|
22
|
+
const MockVariableInput = ({ onChange, metaTree }: any) => {
|
|
23
|
+
if (metaTree) {
|
|
24
|
+
captured.metaTrees.push(metaTree);
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
data-testid="variable-input"
|
|
30
|
+
onClick={() =>
|
|
31
|
+
onChange?.('title', {
|
|
32
|
+
interface: 'input',
|
|
33
|
+
uiSchema: { 'x-component': 'Input' },
|
|
34
|
+
paths: ['collection', 'title'],
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
mock-variable-input
|
|
39
|
+
</button>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
return { ...actual, VariableInput: MockVariableInput };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// $context.data (not an association) -> category (m2o) -> book (m2o) -> createdBy (not counted) -> departments (m2m).
|
|
46
|
+
// Mirrors a workflow collection-event trigger that preloads `category.book.createdBy.departments`.
|
|
47
|
+
function buildRightMetaTree(): MetaTreeNode[] {
|
|
48
|
+
const departments: MetaTreeNode = {
|
|
49
|
+
name: 'departments',
|
|
50
|
+
title: 'Departments',
|
|
51
|
+
type: 'object',
|
|
52
|
+
interface: 'm2m',
|
|
53
|
+
paths: ['$context', 'data', 'category', 'book', 'createdBy', 'departments'],
|
|
54
|
+
};
|
|
55
|
+
const createdBy: MetaTreeNode = {
|
|
56
|
+
name: 'createdBy',
|
|
57
|
+
title: 'Created by',
|
|
58
|
+
type: 'object',
|
|
59
|
+
interface: 'createdBy',
|
|
60
|
+
paths: ['$context', 'data', 'category', 'book', 'createdBy'],
|
|
61
|
+
children: async () => [departments],
|
|
62
|
+
};
|
|
63
|
+
const book: MetaTreeNode = {
|
|
64
|
+
name: 'book',
|
|
65
|
+
title: 'Book',
|
|
66
|
+
type: 'object',
|
|
67
|
+
interface: 'obo',
|
|
68
|
+
paths: ['$context', 'data', 'category', 'book'],
|
|
69
|
+
children: async () => [createdBy],
|
|
70
|
+
};
|
|
71
|
+
const category: MetaTreeNode = {
|
|
72
|
+
name: 'category',
|
|
73
|
+
title: 'Category',
|
|
74
|
+
type: 'object',
|
|
75
|
+
interface: 'm2o',
|
|
76
|
+
paths: ['$context', 'data', 'category'],
|
|
77
|
+
children: async () => [book],
|
|
78
|
+
};
|
|
79
|
+
const data: MetaTreeNode = {
|
|
80
|
+
name: 'data',
|
|
81
|
+
title: 'Trigger data',
|
|
82
|
+
type: 'object',
|
|
83
|
+
paths: ['$context', 'data'],
|
|
84
|
+
children: async () => [category],
|
|
85
|
+
};
|
|
86
|
+
return [{ name: '$context', title: 'Trigger variables', type: '', paths: ['$context'], children: [data] }];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function resolveChildren(node: MetaTreeNode | undefined) {
|
|
90
|
+
const children = node?.children;
|
|
91
|
+
if (typeof children === 'function') {
|
|
92
|
+
return (await (children as () => Promise<MetaTreeNode[]>)()) ?? [];
|
|
93
|
+
}
|
|
94
|
+
return Array.isArray(children) ? children : [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function walkToCreatedByChildren(metaTree: any) {
|
|
98
|
+
const roots: MetaTreeNode[] = typeof metaTree === 'function' ? await metaTree() : metaTree;
|
|
99
|
+
const context = roots.find((node) => node.name === '$context');
|
|
100
|
+
const data = (await resolveChildren(context)).find((node) => node.name === 'data');
|
|
101
|
+
const category = (await resolveChildren(data)).find((node) => node.name === 'category');
|
|
102
|
+
const book = (await resolveChildren(category)).find((node) => node.name === 'book');
|
|
103
|
+
const createdBy = (await resolveChildren(book)).find((node) => node.name === 'createdBy');
|
|
104
|
+
return (await resolveChildren(createdBy)).map((node) => node.name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createModel() {
|
|
108
|
+
const engine = new FlowEngine();
|
|
109
|
+
const model = new FlowModel({ uid: 'm-variable-filter-right', flowEngine: engine });
|
|
110
|
+
const app = createMockFlowApp();
|
|
111
|
+
model.context.defineProperty('app', { value: app });
|
|
112
|
+
|
|
113
|
+
class InputInterface extends TestCollectionFieldInterface {
|
|
114
|
+
name = 'input';
|
|
115
|
+
group = 'basic';
|
|
116
|
+
filterable = { operators: [{ value: '$eq', label: 'Equals' }] };
|
|
117
|
+
}
|
|
118
|
+
app.addFieldInterfaces([InputInterface]);
|
|
119
|
+
|
|
120
|
+
const ds = engine.dataSourceManager.getDataSource('main');
|
|
121
|
+
ds.addCollection({
|
|
122
|
+
name: 'posts',
|
|
123
|
+
fields: [{ name: 'title', type: 'string', interface: 'input', uiSchema: { 'x-component': 'Input' } }],
|
|
124
|
+
});
|
|
125
|
+
model.context.defineProperty('collection', { get: () => ds.getCollection('posts') });
|
|
126
|
+
|
|
127
|
+
return model as any;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function renderItem(props: Record<string, unknown>) {
|
|
131
|
+
const value = { path: 'title', operator: '$eq', value: '' } as any;
|
|
132
|
+
const model = createModel();
|
|
133
|
+
render(
|
|
134
|
+
<VariableFilterItem value={value} model={model} rightAsVariable rightMetaTree={buildRightMetaTree()} {...props} />,
|
|
135
|
+
);
|
|
136
|
+
fireEvent.click(screen.getAllByTestId('variable-input')[0]);
|
|
137
|
+
return captured.metaTrees.at(-1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
describe('VariableFilterItem right-side association depth', () => {
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
document.body.innerHTML = '';
|
|
143
|
+
captured.metaTrees = [];
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('applies maxAssociationFieldDepth to the right tree by default', async () => {
|
|
147
|
+
const metaTree = renderItem({ maxAssociationFieldDepth: 2 });
|
|
148
|
+
// `departments` is the third association down the path, so the default cap removes it.
|
|
149
|
+
expect(await walkToCreatedByChildren(metaTree)).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// The right side is a variable tree whose depth is decided by its provider (in workflow, by the trigger's
|
|
153
|
+
// "Preload associations" config). Capping it by the left-side field-picker limit hid preloaded variables.
|
|
154
|
+
it('leaves the right tree untouched when rightMaxAssociationFieldDepth is null', async () => {
|
|
155
|
+
const metaTree = renderItem({ maxAssociationFieldDepth: 2, rightMaxAssociationFieldDepth: null });
|
|
156
|
+
expect(await walkToCreatedByChildren(metaTree)).toEqual(['departments']);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -47,6 +47,10 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
47
47
|
return { ...actual, VariableInput: MockVariableInput };
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
vi.mock('../../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent', () => ({
|
|
51
|
+
DateFilterDynamicComponent: () => <div data-testid="date-filter-dynamic-component" />,
|
|
52
|
+
}));
|
|
53
|
+
|
|
50
54
|
const getRenderedSelectTexts = (root: ParentNode = document.body) =>
|
|
51
55
|
Array.from(root.querySelectorAll('.ant-select-selection-item')).map((node) => (node.textContent || '').trim());
|
|
52
56
|
|
|
@@ -158,6 +162,127 @@ describe('VariableFilterItem', () => {
|
|
|
158
162
|
expect(value.value).toBe('abc');
|
|
159
163
|
});
|
|
160
164
|
|
|
165
|
+
it.each([
|
|
166
|
+
{ operator: '$includes', xComponent: 'Input' },
|
|
167
|
+
{ operator: '$eq', xComponent: 'Select' },
|
|
168
|
+
{ operator: '$in', xComponent: 'Select' },
|
|
169
|
+
])('uses a silent app registry lookup for the local $xComponent control', async ({ operator, xComponent }) => {
|
|
170
|
+
const value = observable({ path: '', operator: '', value: '' }) as VariableFilterItemValue;
|
|
171
|
+
const model = CreateModel();
|
|
172
|
+
const app = model.context.app as unknown as ReturnType<typeof createMockFlowApp>;
|
|
173
|
+
const getComponent = vi.spyOn(app, 'getComponent');
|
|
174
|
+
|
|
175
|
+
(globalThis as { __TEST_PATH__?: string }).__TEST_PATH__ = 'status';
|
|
176
|
+
(globalThis as { __TEST_META__?: MetaTreeNode }).__TEST_META__ = {
|
|
177
|
+
interface: 'input',
|
|
178
|
+
uiSchema: {
|
|
179
|
+
'x-component': xComponent,
|
|
180
|
+
enum: [{ label: 'Draft', value: 'draft' }],
|
|
181
|
+
'x-filter-operators': [
|
|
182
|
+
{
|
|
183
|
+
value: operator,
|
|
184
|
+
label: 'Operator',
|
|
185
|
+
selected: true,
|
|
186
|
+
schema: {
|
|
187
|
+
'x-component': xComponent,
|
|
188
|
+
'x-component-props': xComponent === 'Select' ? { mode: operator === '$in' ? 'tags' : null } : {},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
paths: ['collection', 'status'],
|
|
194
|
+
name: 'status',
|
|
195
|
+
title: 'Status',
|
|
196
|
+
type: 'string',
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
|
|
200
|
+
fireEvent.click(screen.getByTestId('variable-input'));
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(value.operator).toBe(operator);
|
|
204
|
+
});
|
|
205
|
+
if (xComponent === 'Select' && operator === '$eq') {
|
|
206
|
+
expect(getComponent).not.toHaveBeenCalled();
|
|
207
|
+
} else {
|
|
208
|
+
expect(getComponent).toHaveBeenCalledWith(xComponent, false);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('silently handles the DatePicker field schema before the date operator component takes over', async () => {
|
|
213
|
+
const value = observable({ path: '', operator: '', value: '' }) as VariableFilterItemValue;
|
|
214
|
+
const model = CreateModel();
|
|
215
|
+
const app = model.context.app as unknown as ReturnType<typeof createMockFlowApp>;
|
|
216
|
+
const getComponent = vi.spyOn(app, 'getComponent');
|
|
217
|
+
|
|
218
|
+
(globalThis as { __TEST_PATH__?: string }).__TEST_PATH__ = 'createdAt';
|
|
219
|
+
(globalThis as { __TEST_META__?: MetaTreeNode }).__TEST_META__ = {
|
|
220
|
+
interface: 'createdAt',
|
|
221
|
+
uiSchema: {
|
|
222
|
+
'x-component': 'DatePicker',
|
|
223
|
+
'x-filter-operators': [
|
|
224
|
+
{
|
|
225
|
+
value: '$dateOn',
|
|
226
|
+
label: 'Is',
|
|
227
|
+
selected: true,
|
|
228
|
+
schema: {
|
|
229
|
+
'x-component': 'DateFilterDynamicComponent',
|
|
230
|
+
'x-component-props': { isRange: false },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
paths: ['collection', 'createdAt'],
|
|
236
|
+
name: 'createdAt',
|
|
237
|
+
title: 'Created at',
|
|
238
|
+
type: 'date',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
|
|
242
|
+
fireEvent.click(screen.getByTestId('variable-input'));
|
|
243
|
+
|
|
244
|
+
await waitFor(() => {
|
|
245
|
+
expect(value.operator).toBe('$dateOn');
|
|
246
|
+
});
|
|
247
|
+
expect(getComponent).toHaveBeenCalledWith('DatePicker', false);
|
|
248
|
+
expect(await screen.findByTestId('date-filter-dynamic-component')).toBeInTheDocument();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('continues resolving plugin-provided keyword controls from the app registry', async () => {
|
|
252
|
+
const value = observable({ path: '', operator: '', value: '' }) as VariableFilterItemValue;
|
|
253
|
+
const model = CreateModel();
|
|
254
|
+
const app = model.context.app as unknown as ReturnType<typeof createMockFlowApp>;
|
|
255
|
+
const MultipleKeywordsInput: React.FC = () => <div data-testid="multiple-keywords-input" />;
|
|
256
|
+
app.addComponents({ MultipleKeywordsInput });
|
|
257
|
+
const getComponent = vi.spyOn(app, 'getComponent');
|
|
258
|
+
|
|
259
|
+
(globalThis as { __TEST_PATH__?: string }).__TEST_PATH__ = 'name';
|
|
260
|
+
(globalThis as { __TEST_META__?: MetaTreeNode }).__TEST_META__ = {
|
|
261
|
+
interface: 'input',
|
|
262
|
+
uiSchema: {
|
|
263
|
+
'x-component': 'Input',
|
|
264
|
+
'x-filter-operators': [
|
|
265
|
+
{
|
|
266
|
+
value: '$in',
|
|
267
|
+
label: 'Is any of',
|
|
268
|
+
selected: true,
|
|
269
|
+
schema: { 'x-component': 'MultipleKeywordsInput' },
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
paths: ['collection', 'name'],
|
|
274
|
+
name: 'name',
|
|
275
|
+
title: 'Name',
|
|
276
|
+
type: 'string',
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
|
|
280
|
+
fireEvent.click(screen.getByTestId('variable-input'));
|
|
281
|
+
|
|
282
|
+
expect(await screen.findByTestId('multiple-keywords-input')).toBeTruthy();
|
|
283
|
+
expect(getComponent).toHaveBeenCalledWith('MultipleKeywordsInput');
|
|
284
|
+
});
|
|
285
|
+
|
|
161
286
|
it('passes disabled through to the left selector, operator select, and right variable input', async () => {
|
|
162
287
|
const value: VariableFilterItemValue = { path: '', operator: '', value: '' };
|
|
163
288
|
const model = CreateModel();
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useFlowModel } from '@nocobase/flow-engine';
|
|
11
|
-
import {
|
|
11
|
+
import { useMemoizedFn } from 'ahooks';
|
|
12
|
+
import { Button, Result } from 'antd';
|
|
12
13
|
import _ from 'lodash';
|
|
13
14
|
import React, { useMemo } from 'react';
|
|
14
15
|
import { useTranslation } from 'react-i18next';
|
|
15
16
|
import { BlockItemCard } from '../BlockItemCard';
|
|
16
|
-
import { BlockModel } from '../../models/base/BlockModel';
|
|
17
|
+
import type { BlockModel } from '../../models/base/BlockModel';
|
|
17
18
|
|
|
18
19
|
function getResourceSettingsInitParams(model: any) {
|
|
19
20
|
if (typeof model?.getResourceSettingsInitParams === 'function') {
|
|
@@ -43,26 +44,46 @@ function getBlockResourceInfo(model: any, t: (key: string) => string) {
|
|
|
43
44
|
const dataSourceName = dataSource ? t(dataSource.displayName || dataSource.key) : dataSourceKey;
|
|
44
45
|
const collectionLabel = collectionName ? `${t(collectionName) || collectionName}` : '';
|
|
45
46
|
const dataSourceLabel = dataSourceName ? `${t(dataSourceName)} > ` : '';
|
|
47
|
+
const isDataSourceLoadFailed = ['loading-failed', 'reloading-failed'].includes(dataSource?.status);
|
|
46
48
|
|
|
47
49
|
return {
|
|
48
50
|
dataSourceName,
|
|
49
51
|
nameValue: `${dataSourceLabel}${collectionLabel}`,
|
|
50
|
-
|
|
52
|
+
isDataSourceLoadFailed,
|
|
53
|
+
isDataSourceUnavailable: Boolean(dataSourceKey && (!dataSource || isDataSourceLoadFailed)),
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
function
|
|
57
|
+
function getTemporarilyUnavailableMessage(t: (key: string, options?: Record<string, string>) => string, name?: string) {
|
|
58
|
+
return name
|
|
59
|
+
? t('The data source "{{name}}" is temporarily unavailable. Please try again later or contact an administrator.', {
|
|
60
|
+
name,
|
|
61
|
+
})
|
|
62
|
+
: t('The data source is temporarily unavailable. Please try again later or contact an administrator.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function DataSourceUnavailablePlaceholder({
|
|
66
|
+
dataSourceName,
|
|
67
|
+
isLoadFailed,
|
|
68
|
+
}: {
|
|
69
|
+
dataSourceName?: string;
|
|
70
|
+
isLoadFailed?: boolean;
|
|
71
|
+
}) {
|
|
55
72
|
const { t } = useTranslation();
|
|
56
|
-
const subTitle =
|
|
57
|
-
? t
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
const subTitle = isLoadFailed
|
|
74
|
+
? getTemporarilyUnavailableMessage(t, dataSourceName)
|
|
75
|
+
: dataSourceName
|
|
76
|
+
? t(
|
|
77
|
+
'The data source "{{name}}" used by this block is disabled or unavailable. Enable the data source to display this block.',
|
|
78
|
+
{ name: dataSourceName },
|
|
79
|
+
)
|
|
80
|
+
: t(
|
|
81
|
+
'The data source used by this block is disabled or unavailable. Enable the data source to display this block.',
|
|
82
|
+
);
|
|
62
83
|
|
|
63
84
|
return (
|
|
64
|
-
<BlockItemCard>
|
|
65
|
-
<Result status=
|
|
85
|
+
<BlockItemCard role="alert" aria-live="polite">
|
|
86
|
+
<Result status={isLoadFailed ? 'warning' : '403'} subTitle={subTitle}></Result>
|
|
66
87
|
</BlockItemCard>
|
|
67
88
|
);
|
|
68
89
|
}
|
|
@@ -70,7 +91,7 @@ function DataSourceUnavailablePlaceholder({ dataSourceName }: { dataSourceName?:
|
|
|
70
91
|
export const BlockPlaceholder = () => {
|
|
71
92
|
const { t } = useTranslation();
|
|
72
93
|
const model: BlockModel = useFlowModel();
|
|
73
|
-
const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
|
|
94
|
+
const { dataSourceName, isDataSourceLoadFailed, isDataSourceUnavailable, nameValue } = useMemo(() => {
|
|
74
95
|
return getBlockResourceInfo(model, t);
|
|
75
96
|
}, [model, t]);
|
|
76
97
|
|
|
@@ -86,10 +107,7 @@ export const BlockPlaceholder = () => {
|
|
|
86
107
|
}, [actionName, nameValue, t]);
|
|
87
108
|
|
|
88
109
|
if (isDataSourceUnavailable) {
|
|
89
|
-
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
|
|
110
|
+
return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} isLoadFailed={isDataSourceLoadFailed} />;
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
return (
|
|
@@ -101,8 +119,8 @@ export const BlockPlaceholder = () => {
|
|
|
101
119
|
|
|
102
120
|
export function BlockDeletePlaceholder() {
|
|
103
121
|
const { t } = useTranslation();
|
|
104
|
-
const model:
|
|
105
|
-
const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
|
|
122
|
+
const model: BlockModel = useFlowModel();
|
|
123
|
+
const { dataSourceName, isDataSourceLoadFailed, isDataSourceUnavailable, nameValue } = useMemo(() => {
|
|
106
124
|
return getBlockResourceInfo(model, t);
|
|
107
125
|
}, [model, t]);
|
|
108
126
|
|
|
@@ -115,10 +133,7 @@ export function BlockDeletePlaceholder() {
|
|
|
115
133
|
}, [nameValue, t]);
|
|
116
134
|
|
|
117
135
|
if (isDataSourceUnavailable) {
|
|
118
|
-
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
|
|
136
|
+
return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} isLoadFailed={isDataSourceLoadFailed} />;
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
return (
|
|
@@ -127,3 +142,35 @@ export function BlockDeletePlaceholder() {
|
|
|
127
142
|
</BlockItemCard>
|
|
128
143
|
);
|
|
129
144
|
}
|
|
145
|
+
|
|
146
|
+
type RefreshableBlockModel = BlockModel & {
|
|
147
|
+
refresh?: () => Promise<unknown> | unknown;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export function BlockResourceErrorPlaceholder() {
|
|
151
|
+
const { t } = useTranslation();
|
|
152
|
+
const model = useFlowModel() as RefreshableBlockModel;
|
|
153
|
+
const { dataSourceName } = useMemo(() => getBlockResourceInfo(model, t), [model, t]);
|
|
154
|
+
const handleRetry = useMemoizedFn(async () => {
|
|
155
|
+
try {
|
|
156
|
+
await model.refresh?.();
|
|
157
|
+
} catch {
|
|
158
|
+
// The resource retains the latest error and keeps this placeholder visible.
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<BlockItemCard ref={model.context.ref} {...model.decoratorProps} role="alert" aria-live="polite">
|
|
164
|
+
<Result
|
|
165
|
+
status="warning"
|
|
166
|
+
title={t('Data loading failed')}
|
|
167
|
+
subTitle={getTemporarilyUnavailableMessage(t, dataSourceName)}
|
|
168
|
+
extra={
|
|
169
|
+
<Button type="primary" onClick={handleRetry}>
|
|
170
|
+
{t('Try again')}
|
|
171
|
+
</Button>
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
174
|
+
</BlockItemCard>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowModelProvider } from '@nocobase/flow-engine';
|
|
11
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
12
12
|
import React from 'react';
|
|
13
|
-
import { BlockDeletePlaceholder } from '../BlockPlaceholder';
|
|
13
|
+
import { BlockDeletePlaceholder, BlockResourceErrorPlaceholder } from '../BlockPlaceholder';
|
|
14
14
|
|
|
15
15
|
vi.mock('react-i18next', () => ({
|
|
16
16
|
useTranslation: () => ({
|
|
@@ -23,8 +23,9 @@ vi.mock('react-i18next', () => ({
|
|
|
23
23
|
function createModel(options: {
|
|
24
24
|
dataSourceKey?: string;
|
|
25
25
|
collectionName?: string;
|
|
26
|
-
dataSource?: { key: string; displayName?: string };
|
|
26
|
+
dataSource?: { key: string; displayName?: string; status?: string };
|
|
27
27
|
flowSettingsEnabled?: boolean;
|
|
28
|
+
refresh?: () => Promise<unknown>;
|
|
28
29
|
}) {
|
|
29
30
|
const model: any = {
|
|
30
31
|
context: {
|
|
@@ -38,6 +39,7 @@ function createModel(options: {
|
|
|
38
39
|
dataSourceKey: options.dataSourceKey,
|
|
39
40
|
collectionName: options.collectionName,
|
|
40
41
|
}),
|
|
42
|
+
refresh: options.refresh,
|
|
41
43
|
};
|
|
42
44
|
model.context.blockModel = model;
|
|
43
45
|
return model;
|
|
@@ -64,25 +66,47 @@ describe('BlockPlaceholder', () => {
|
|
|
64
66
|
).toBeInTheDocument();
|
|
65
67
|
});
|
|
66
68
|
|
|
67
|
-
it('should
|
|
69
|
+
it('should render data source unavailable placeholder outside configuration mode', () => {
|
|
68
70
|
const model = createModel({
|
|
69
71
|
dataSourceKey: 'external-mysql',
|
|
70
72
|
collectionName: 'orders',
|
|
71
73
|
flowSettingsEnabled: false,
|
|
72
74
|
});
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
render(
|
|
75
77
|
<FlowModelProvider model={model}>
|
|
76
78
|
<BlockDeletePlaceholder />
|
|
77
79
|
</FlowModelProvider>,
|
|
78
80
|
);
|
|
79
81
|
|
|
80
82
|
expect(
|
|
81
|
-
screen.
|
|
83
|
+
screen.getByText(
|
|
82
84
|
'The data source "external-mysql" used by this block is disabled or unavailable. Enable the data source to display this block.',
|
|
83
85
|
),
|
|
86
|
+
).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should treat a failed data source as unavailable', () => {
|
|
90
|
+
const model = createModel({
|
|
91
|
+
dataSourceKey: 'external-mysql',
|
|
92
|
+
collectionName: 'orders',
|
|
93
|
+
dataSource: { key: 'external-mysql', displayName: 'External MySQL', status: 'loading-failed' },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
render(
|
|
97
|
+
<FlowModelProvider model={model}>
|
|
98
|
+
<BlockDeletePlaceholder />
|
|
99
|
+
</FlowModelProvider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(
|
|
103
|
+
screen.getByText(
|
|
104
|
+
'The data source "External MySQL" is temporarily unavailable. Please try again later or contact an administrator.',
|
|
105
|
+
),
|
|
106
|
+
).toBeInTheDocument();
|
|
107
|
+
expect(
|
|
108
|
+
screen.queryByText('The Collection "External MySQL > orders" may have been deleted. Please remove this Block.'),
|
|
84
109
|
).not.toBeInTheDocument();
|
|
85
|
-
expect(container).toBeEmptyDOMElement();
|
|
86
110
|
});
|
|
87
111
|
|
|
88
112
|
it('should keep collection deleted placeholder when the data source is available', () => {
|
|
@@ -102,4 +126,30 @@ describe('BlockPlaceholder', () => {
|
|
|
102
126
|
screen.getByText('The Collection "External MySQL > orders" may have been deleted. Please remove this Block.'),
|
|
103
127
|
).toBeInTheDocument();
|
|
104
128
|
});
|
|
129
|
+
|
|
130
|
+
it('should render a retryable resource error placeholder', async () => {
|
|
131
|
+
const refresh = vi.fn().mockRejectedValue(new Error('still unavailable'));
|
|
132
|
+
const model = createModel({
|
|
133
|
+
dataSourceKey: 'external-mysql',
|
|
134
|
+
collectionName: 'orders',
|
|
135
|
+
dataSource: { key: 'external-mysql', displayName: 'External MySQL' },
|
|
136
|
+
refresh,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
render(
|
|
140
|
+
<FlowModelProvider model={model}>
|
|
141
|
+
<BlockResourceErrorPlaceholder />
|
|
142
|
+
</FlowModelProvider>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(screen.getByText('Data loading failed')).toBeInTheDocument();
|
|
146
|
+
expect(
|
|
147
|
+
screen.getByText(
|
|
148
|
+
'The data source "External MySQL" is temporarily unavailable. Please try again later or contact an administrator.',
|
|
149
|
+
),
|
|
150
|
+
).toBeInTheDocument();
|
|
151
|
+
|
|
152
|
+
fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
|
|
153
|
+
await waitFor(() => expect(refresh).toHaveBeenCalledTimes(1));
|
|
154
|
+
});
|
|
105
155
|
});
|
package/src/flow/index.ts
CHANGED
|
@@ -22,12 +22,14 @@ import { Markdown } from './common/Markdown/Markdown';
|
|
|
22
22
|
import { LiquidEngine } from './common/Liquid';
|
|
23
23
|
import type { PreviewRunJSResult } from './components/code-editor/runjsDiagnostics';
|
|
24
24
|
import { TextAreaWithContextSelector } from './components/TextAreaWithContextSelector';
|
|
25
|
+
import { registerDeviceTypeContext } from './internal/registerDeviceTypeContext';
|
|
25
26
|
|
|
26
27
|
export class PluginFlowEngine<TApp extends BaseApplication<any> = BaseApplication<any>> extends Plugin<
|
|
27
28
|
PluginOptions<any>,
|
|
28
29
|
TApp
|
|
29
30
|
> {
|
|
30
31
|
async load() {
|
|
32
|
+
registerDeviceTypeContext(this.flowEngine);
|
|
31
33
|
this.app.addComponents({ FlowRoute });
|
|
32
34
|
this.app.flowEngine.setModelRepository(new FlowModelRepository(this.app));
|
|
33
35
|
const filteredModels = Object.fromEntries(
|