@nocobase/client-v2 2.1.0-beta.45 → 2.1.0-beta.46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/client-v2",
3
- "version": "2.1.0-beta.45",
3
+ "version": "2.1.0-beta.46",
4
4
  "license": "Apache-2.0",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.mjs",
@@ -27,11 +27,11 @@
27
27
  "@formily/antd-v5": "1.2.3",
28
28
  "@formily/react": "^2.2.27",
29
29
  "@formily/shared": "^2.2.27",
30
- "@nocobase/evaluators": "2.1.0-beta.45",
31
- "@nocobase/flow-engine": "2.1.0-beta.45",
32
- "@nocobase/sdk": "2.1.0-beta.45",
33
- "@nocobase/shared": "2.1.0-beta.45",
34
- "@nocobase/utils": "2.1.0-beta.45",
30
+ "@nocobase/evaluators": "2.1.0-beta.46",
31
+ "@nocobase/flow-engine": "2.1.0-beta.46",
32
+ "@nocobase/sdk": "2.1.0-beta.46",
33
+ "@nocobase/shared": "2.1.0-beta.46",
34
+ "@nocobase/utils": "2.1.0-beta.46",
35
35
  "ahooks": "^3.7.2",
36
36
  "antd": "5.24.2",
37
37
  "antd-style": "3.7.1",
@@ -45,5 +45,5 @@
45
45
  "react-i18next": "^11.15.1",
46
46
  "react-router-dom": "^6.30.1"
47
47
  },
48
- "gitHead": "42587115fc34c3eb01ef2b2549f1c998e5708318"
48
+ "gitHead": "91fd3ab238a5ff58735bbfca04a8cb05d233b0af"
49
49
  }
@@ -100,4 +100,13 @@ describe.each(browserCheckerCases)('$label', ({ scriptPath }) => {
100
100
 
101
101
  expect(replace).not.toHaveBeenCalled();
102
102
  });
103
+
104
+ it('does not redirect the root path when the normalized basename is also root', () => {
105
+ const replace = executeBrowserChecker(scriptPath, {
106
+ pathname: '/',
107
+ publicPath: '/',
108
+ });
109
+
110
+ expect(replace).not.toHaveBeenCalled();
111
+ });
103
112
  });
@@ -0,0 +1,22 @@
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 { sortingRule } from '../sortingRules';
11
+
12
+ describe('sortingRule', () => {
13
+ it('should not throw when the block collection is unavailable', () => {
14
+ const schema = sortingRule.uiSchema({
15
+ blockModel: {
16
+ collection: undefined,
17
+ },
18
+ } as any);
19
+
20
+ expect(schema.sort.items.properties.space.properties.field.enum).toEqual([]);
21
+ });
22
+ });
@@ -33,9 +33,8 @@ export const sortingRule = defineAction({
33
33
  name: 'sortingRule',
34
34
  title: tExpr('Default sorting'),
35
35
  uiSchema: (ctx) => {
36
- const fields = ctx.collectionField?.targetCollection
37
- ? ctx.collectionField.targetCollection.getFields()
38
- : ctx.blockModel.collection.getFields();
36
+ const collection = ctx.collectionField?.targetCollection || ctx.blockModel?.collection;
37
+ const fields = typeof collection?.getFields === 'function' ? collection.getFields() : [];
39
38
  const sortFields = getSortFields(fields);
40
39
  return {
41
40
  sort: {
@@ -109,7 +108,7 @@ export const sortingRule = defineAction({
109
108
  };
110
109
  },
111
110
  async handler(ctx, params) {
112
- const sortArr = params.sort.map((item) => {
111
+ const sortArr = (params.sort || []).map((item) => {
113
112
  return item.direction === 'desc' ? `-${item.field}` : item.field;
114
113
  });
115
114
  // @ts-ignore
@@ -25,6 +25,7 @@ const buildLinkSettingSchema = (t: (title: any) => any) => ({
25
25
  'x-decorator': 'FormItem',
26
26
  'x-component': 'FlowSettingsVariableTextArea',
27
27
  'x-component-props': {
28
+ placeholder: t('https://www.example.com'),
28
29
  rows: 1,
29
30
  maxRows: 1,
30
31
  },
@@ -15,19 +15,66 @@ import { useTranslation } from 'react-i18next';
15
15
  import { BlockItemCard } from '../BlockItemCard';
16
16
  import { BlockModel } from '../../models/base/BlockModel';
17
17
 
18
+ function getResourceSettingsInitParams(model: any) {
19
+ if (typeof model?.getResourceSettingsInitParams === 'function') {
20
+ return model.getResourceSettingsInitParams();
21
+ }
22
+ if (typeof model?.getStepParams === 'function') {
23
+ return model.getStepParams('resourceSettings', 'init');
24
+ }
25
+ return undefined;
26
+ }
27
+
28
+ function getBlockResourceInfo(model: any, t: (key: string) => string) {
29
+ const blockModel = model?.context?.blockModel || model;
30
+ const params = getResourceSettingsInitParams(blockModel) || {};
31
+ const collection = blockModel?.context?.collection || blockModel?.collection || model?.context?.collection;
32
+ const dataSource =
33
+ collection?.dataSource || blockModel?.context?.dataSource || blockModel?.dataSource || model?.dataSource;
34
+ const dataSourceKey = dataSource?.key || collection?.dataSourceKey || params.dataSourceKey;
35
+ const collectionName =
36
+ collection?.title ||
37
+ collection?.name ||
38
+ collection?.tableName ||
39
+ model?.resource?.resourceName ||
40
+ model?.resource?.getResourceName?.() ||
41
+ params.associationName ||
42
+ params.collectionName;
43
+ const dataSourceName = dataSource ? t(dataSource.displayName || dataSource.key) : dataSourceKey;
44
+ const collectionLabel = collectionName ? `${t(collectionName) || collectionName}` : '';
45
+ const dataSourceLabel = dataSourceName ? `${t(dataSourceName)} > ` : '';
46
+
47
+ return {
48
+ dataSourceName,
49
+ nameValue: `${dataSourceLabel}${collectionLabel}`,
50
+ isDataSourceUnavailable: Boolean(dataSourceKey && !dataSource),
51
+ };
52
+ }
53
+
54
+ function DataSourceUnavailablePlaceholder({ dataSourceName }: { dataSourceName?: string }) {
55
+ const { t } = useTranslation();
56
+ const subTitle = dataSourceName
57
+ ? t(
58
+ 'The data source "{{name}}" used by this block is disabled or unavailable. Enable the data source to display this block.',
59
+ { name: dataSourceName },
60
+ )
61
+ : t('The data source used by this block is disabled or unavailable. Enable the data source to display this block.');
62
+
63
+ return (
64
+ <BlockItemCard>
65
+ <Result status="403" subTitle={subTitle}></Result>
66
+ </BlockItemCard>
67
+ );
68
+ }
69
+
18
70
  export const BlockPlaceholder = () => {
19
71
  const { t } = useTranslation();
20
72
  const model: BlockModel = useFlowModel();
21
- const blockModel = model.context.blockModel;
22
- const collection = blockModel.collection;
23
- const dataSource = collection.dataSource;
24
- const nameValue = useMemo(() => {
25
- const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
26
- const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} ` : '';
27
- return `${dataSourcePrefix}${collectionPrefix}`;
28
- }, []);
73
+ const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
74
+ return getBlockResourceInfo(model, t);
75
+ }, [model, t]);
29
76
 
30
- const { actionName } = model.forbidden;
77
+ const { actionName } = model.forbidden || {};
31
78
  const messageValue = useMemo(() => {
32
79
  return t(
33
80
  `The current user only has the UI configuration permission, but don't have "{{actionName}}" permission for collection "{{name}}"`,
@@ -37,6 +84,14 @@ export const BlockPlaceholder = () => {
37
84
  },
38
85
  ).replaceAll('&gt;', '>');
39
86
  }, [actionName, nameValue, t]);
87
+
88
+ if (isDataSourceUnavailable) {
89
+ if (!model.context.flowSettingsEnabled) {
90
+ return null;
91
+ }
92
+ return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
93
+ }
94
+
40
95
  return (
41
96
  <BlockItemCard>
42
97
  <Result status="403" subTitle={messageValue}></Result>
@@ -47,12 +102,10 @@ export const BlockPlaceholder = () => {
47
102
  export function BlockDeletePlaceholder() {
48
103
  const { t } = useTranslation();
49
104
  const model: any = useFlowModel();
50
- const dataSource = model.dataSource;
51
- const nameValue = useMemo(() => {
52
- const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
53
- const collectionPrefix = model.resource.resourceName;
54
- return `${dataSourcePrefix}${collectionPrefix}`;
55
- }, []);
105
+ const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
106
+ return getBlockResourceInfo(model, t);
107
+ }, [model, t]);
108
+
56
109
  const messageValue = useMemo(() => {
57
110
  return t(`The {{type}} "{{name}}" may have been deleted. Please remove this {{blockType}}.`, {
58
111
  type: t('Collection'),
@@ -60,6 +113,14 @@ export function BlockDeletePlaceholder() {
60
113
  blockType: t('Block'),
61
114
  }).replaceAll('&gt;', '>');
62
115
  }, [nameValue, t]);
116
+
117
+ if (isDataSourceUnavailable) {
118
+ if (!model.context.flowSettingsEnabled) {
119
+ return null;
120
+ }
121
+ return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
122
+ }
123
+
63
124
  return (
64
125
  <BlockItemCard>
65
126
  <Result status="404" subTitle={messageValue}></Result>
@@ -0,0 +1,105 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { FlowModelProvider } from '@nocobase/flow-engine';
11
+ import { render, screen } from '@testing-library/react';
12
+ import React from 'react';
13
+ import { BlockDeletePlaceholder } from '../BlockPlaceholder';
14
+
15
+ vi.mock('react-i18next', () => ({
16
+ useTranslation: () => ({
17
+ t: (key: string, values?: Record<string, string>) => {
18
+ return key.replace(/\{\{(\w+)\}\}/g, (_, name) => values?.[name] ?? '');
19
+ },
20
+ }),
21
+ }));
22
+
23
+ function createModel(options: {
24
+ dataSourceKey?: string;
25
+ collectionName?: string;
26
+ dataSource?: { key: string; displayName?: string };
27
+ flowSettingsEnabled?: boolean;
28
+ }) {
29
+ const model: any = {
30
+ context: {
31
+ flowSettingsEnabled: options.flowSettingsEnabled,
32
+ },
33
+ dataSource: options.dataSource,
34
+ resource: {
35
+ resourceName: options.collectionName,
36
+ },
37
+ getResourceSettingsInitParams: () => ({
38
+ dataSourceKey: options.dataSourceKey,
39
+ collectionName: options.collectionName,
40
+ }),
41
+ };
42
+ model.context.blockModel = model;
43
+ return model;
44
+ }
45
+
46
+ describe('BlockPlaceholder', () => {
47
+ it('should render data source unavailable placeholder in configuration mode when the configured data source is missing', () => {
48
+ const model = createModel({
49
+ dataSourceKey: 'external-mysql',
50
+ collectionName: 'orders',
51
+ flowSettingsEnabled: true,
52
+ });
53
+
54
+ render(
55
+ <FlowModelProvider model={model}>
56
+ <BlockDeletePlaceholder />
57
+ </FlowModelProvider>,
58
+ );
59
+
60
+ expect(
61
+ screen.getByText(
62
+ 'The data source "external-mysql" used by this block is disabled or unavailable. Enable the data source to display this block.',
63
+ ),
64
+ ).toBeInTheDocument();
65
+ });
66
+
67
+ it('should hide data source unavailable block outside configuration mode', () => {
68
+ const model = createModel({
69
+ dataSourceKey: 'external-mysql',
70
+ collectionName: 'orders',
71
+ flowSettingsEnabled: false,
72
+ });
73
+
74
+ const { container } = render(
75
+ <FlowModelProvider model={model}>
76
+ <BlockDeletePlaceholder />
77
+ </FlowModelProvider>,
78
+ );
79
+
80
+ expect(
81
+ screen.queryByText(
82
+ 'The data source "external-mysql" used by this block is disabled or unavailable. Enable the data source to display this block.',
83
+ ),
84
+ ).not.toBeInTheDocument();
85
+ expect(container).toBeEmptyDOMElement();
86
+ });
87
+
88
+ it('should keep collection deleted placeholder when the data source is available', () => {
89
+ const model = createModel({
90
+ dataSourceKey: 'external-mysql',
91
+ collectionName: 'orders',
92
+ dataSource: { key: 'external-mysql', displayName: 'External MySQL' },
93
+ });
94
+
95
+ render(
96
+ <FlowModelProvider model={model}>
97
+ <BlockDeletePlaceholder />
98
+ </FlowModelProvider>,
99
+ );
100
+
101
+ expect(
102
+ screen.getByText('The Collection "External MySQL > orders" may have been deleted. Please remove this Block.'),
103
+ ).toBeInTheDocument();
104
+ });
105
+ });
@@ -541,19 +541,23 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
541
541
 
542
542
  protected defaultBlockTitle() {
543
543
  const blockLabel = this.translate(this.constructor['meta']?.label || this.constructor.name);
544
- const dsName = this.dataSource?.displayName || this.dataSource?.key;
544
+ const params = this.getResourceSettingsInitParams();
545
+ const dataSource = this.dataSource;
546
+ const collection = this.collection;
547
+ const dsName = dataSource?.displayName || dataSource?.key || params?.dataSourceKey;
545
548
  const dsCount = this.context?.dataSourceManager?.getDataSources?.().length || 0;
546
- const colTitle = this.collection?.title;
547
- if (this.association) {
549
+ const colTitle = collection?.title || collection?.name || params?.collectionName || '';
550
+ const association = dataSource ? this.association : undefined;
551
+ if (association) {
548
552
  const resourceName = this.resource.getResourceName();
549
- const sourceCollection = this.dataSource.getCollection(resourceName.split('.')[0]);
553
+ const sourceCollection = dataSource.getCollection(resourceName.split('.')[0]);
550
554
  return createDefaultCollectionBlockTitle({
551
555
  blockLabel,
552
556
  dsName,
553
557
  dsCount,
554
558
  collectionTitle: colTitle,
555
- sourceCollectionTitle: sourceCollection.title,
556
- associationTitle: this.association.title,
559
+ sourceCollectionTitle: sourceCollection?.title,
560
+ associationTitle: association.title,
557
561
  });
558
562
  }
559
563
  return createDefaultCollectionBlockTitle({ blockLabel, dsName, dsCount, collectionTitle: colTitle });
@@ -7,8 +7,17 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DataSource, FlowEngine } from '@nocobase/flow-engine';
10
+ import { DataSource, FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
11
11
  import { createDefaultCollectionBlockTitle } from '../../../utils/blockUtils';
12
+ import { CollectionBlockModel } from '../CollectionBlockModel';
13
+
14
+ class MissingCollectionTitleBlockModel extends CollectionBlockModel {
15
+ static meta = { label: 'Table' };
16
+
17
+ createResource(ctx: any, _params: any) {
18
+ return ctx.createResource(MultiRecordResource);
19
+ }
20
+ }
12
21
 
13
22
  function normalizeTitle(s: string) {
14
23
  return (s || '').replace(/\s+/g, ' ').trim();
@@ -92,4 +101,27 @@ describe('CollectionBlockModel default title rule', () => {
92
101
  expect(t).toContain('Table:');
93
102
  expect(t).toContain('Main > Users > Tags (Tags)');
94
103
  });
104
+
105
+ it('uses resource settings as title fallback when the configured data source is unavailable', () => {
106
+ const engine = new FlowEngine();
107
+ engine.registerModels({ MissingCollectionTitleBlockModel });
108
+ const model = engine.createModel<MissingCollectionTitleBlockModel>({
109
+ uid: 'missing-data-source-title-block',
110
+ use: 'MissingCollectionTitleBlockModel',
111
+ stepParams: {
112
+ resourceSettings: {
113
+ init: {
114
+ dataSourceKey: 'mysql',
115
+ collectionName: 'orders',
116
+ },
117
+ },
118
+ },
119
+ });
120
+
121
+ const title = normalizeTitle(model.title);
122
+
123
+ expect(title).toContain('Table:');
124
+ expect(title).toContain('orders');
125
+ expect(title).not.toContain('undefined');
126
+ });
95
127
  });