diff --git a/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx b/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx
index fa4691d33..2f0f3dcee 100644
--- a/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx
+++ b/packages/core/client/src/collection-manager/CollectionManagerShortcut.tsx
@@ -24,6 +24,8 @@ import {
ViewCollectionField,
ViewFieldAction,
SyncSQLFieldsAction,
+ DeleteCollection,
+ DeleteCollectionAction,
} from './Configuration';
import { CollectionCategroriesProvider } from './CollectionManagerProvider';
@@ -71,6 +73,8 @@ export const CollectionManagerPane = () => {
AddCategory,
EditCollection,
EditCollectionAction,
+ DeleteCollection,
+ DeleteCollectionAction,
EditFieldAction,
EditCollectionField,
OverridingCollectionField,
diff --git a/packages/core/client/src/collection-manager/Configuration/DeleteCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/DeleteCollectionAction.tsx
new file mode 100644
index 000000000..b3e7a04f8
--- /dev/null
+++ b/packages/core/client/src/collection-manager/Configuration/DeleteCollectionAction.tsx
@@ -0,0 +1,186 @@
+import React, { useState } from 'react';
+import { css } from '@emotion/css';
+import { useTranslation } from 'react-i18next';
+import { Button, message } from 'antd';
+import { useForm } from '@formily/react';
+import { DeleteOutlined, ExclamationCircleFilled } from '@ant-design/icons';
+import { RecordProvider, useRecord } from '../../record-provider';
+import { ActionContextProvider, SchemaComponent } from '../../schema-component';
+import * as components from './components';
+import { useCollectionManager, useResourceActionContext, useResourceContext, useActionContext } from '../../';
+import { useCancelAction } from '../action-hooks';
+
+export const DeleteCollection = (props) => {
+ const record = useRecord();
+ return ;
+};
+
+export const useDestroyActionAndRefreshCM = () => {
+ const { run } = useDestroyAction();
+ const { refreshCM } = useCollectionManager();
+ return {
+ async run() {
+ await run();
+ await refreshCM();
+ },
+ };
+};
+export const useBulkDestroyActionAndRefreshCM = () => {
+ const { run } = useBulkDestroyAction();
+ const { refreshCM } = useCollectionManager();
+ return {
+ async run() {
+ await run();
+ await refreshCM();
+ },
+ };
+};
+export const useDestroyAction = () => {
+ const { refresh } = useResourceActionContext();
+ const { resource, targetKey } = useResourceContext();
+ const { [targetKey]: filterByTk } = useRecord();
+ const ctx = useActionContext();
+ const form = useForm();
+ const { cascade } = form?.values || {};
+ return {
+ async run() {
+ await resource.destroy({ filterByTk, cascade });
+ ctx?.setVisible?.(false);
+ refresh();
+ },
+ };
+};
+
+export const useBulkDestroyAction = () => {
+ const { state, setState, refresh } = useResourceActionContext();
+ const { resource } = useResourceContext();
+ const ctx = useActionContext();
+ const { t } = useTranslation();
+ const form = useForm();
+ const { cascade } = form?.values || {};
+ return {
+ async run() {
+ if (!state?.selectedRowKeys?.length) {
+ return message.error(t('Please select the records you want to delete'));
+ }
+ await resource.destroy({
+ filterByTk: state?.selectedRowKeys || [],
+ cascade,
+ });
+ form.reset();
+ ctx?.setVisible?.(false);
+ setState?.({ selectedRowKeys: [] });
+ refresh();
+ },
+ };
+};
+export const DeleteCollectionAction = (props) => {
+ const { scope, getContainer, item: record, children, isBulk, useAction, ...otherProps } = props;
+ const { t } = useTranslation();
+ const [visible, setVisible] = useState(false);
+ const getDestroyCollectionAction = () => {
+ if (isBulk) {
+ return useBulkDestroyActionAndRefreshCM;
+ } else {
+ if (useAction) {
+ return useAction;
+ }
+ return useDestroyActionAndRefreshCM;
+ }
+ };
+ const Title = () => {
+ return (
+
+
+ {t('Delete record')}
+
+ );
+ };
+
+ return (
+
+
+ {isBulk ? (
+ } onClick={() => setVisible(true)}>
+ {children || t('Delete')}
+
+ ) : (
+ setVisible(true)} {...otherProps}>
+ {children || t('Delete')}
+
+ )}
+ ,
+ 'x-component-props': {
+ width: 500,
+ getContainer: '{{ getContainer }}',
+ className: css`
+ .ant-modal-body {
+ margin-left: 35px;
+ margin-bottom: 35px;
+ .ant-checkbox-wrapper {
+ height: 25px;
+ }
+ }
+ `,
+ },
+ properties: {
+ info: {
+ type: 'string',
+ 'x-component': 'div',
+ 'x-content': "{{t('Are you sure you want to delete it?')}}",
+ },
+ cascade: {
+ type: 'boolean',
+ 'x-decorator': 'FormItem',
+ 'x-component': 'Checkbox',
+ default: false,
+ 'x-content': t(
+ 'Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects',
+ ),
+ },
+ footer: {
+ type: 'void',
+ 'x-component': 'Action.Modal.Footer',
+ properties: {
+ action1: {
+ title: '{{ t("Cancel") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ useAction: '{{ useCancelAction }}',
+ },
+ },
+ action2: {
+ title: '{{ t("Ok") }}',
+ 'x-component': 'Action',
+ 'x-component-props': {
+ type: 'primary',
+ useAction: '{{ useDestroyCollectionAction }}',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }}
+ components={{ ...components }}
+ scope={{
+ getContainer,
+ useDestroyCollectionAction: getDestroyCollectionAction(),
+ useCancelAction,
+ ...scope,
+ }}
+ />
+
+
+ );
+};
+DeleteCollectionAction.displayName = 'DeleteCollectionAction';
diff --git a/packages/core/client/src/collection-manager/Configuration/index.tsx b/packages/core/client/src/collection-manager/Configuration/index.tsx
index ad184f595..011a1c1eb 100644
--- a/packages/core/client/src/collection-manager/Configuration/index.tsx
+++ b/packages/core/client/src/collection-manager/Configuration/index.tsx
@@ -16,6 +16,7 @@ export * from './AddCategoryAction';
export * from './EditCategoryAction';
export * from './SyncFieldsAction';
export * from './SyncSQLFieldsAction';
+export * from './DeleteCollectionAction';
registerValidateFormats({
uid: /^[A-Za-z0-9][A-Za-z0-9_-]*$/,
diff --git a/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts b/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts
index 0de2a37fd..72f6ec0da 100644
--- a/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts
+++ b/packages/core/client/src/collection-manager/Configuration/schemas/collections.ts
@@ -7,6 +7,7 @@ import { i18n } from '../../../i18n';
import { CollectionOptions } from '../../types';
import { CollectionCategory } from '../components/CollectionCategory';
import { CollectionTemplate } from '../components/CollectionTemplate';
+
const compile = (source) => {
return Schema.compile(source, { t: i18n.t });
};
@@ -147,14 +148,10 @@ export const collectionTableSchema: ISchema = {
delete: {
type: 'void',
title: '{{ t("Delete") }}',
- 'x-component': 'Action',
+ 'x-component': 'DeleteCollection',
'x-component-props': {
- icon: 'DeleteOutlined',
- useAction: '{{ cm.useBulkDestroyActionAndRefreshCM }}',
- confirm: {
- title: "{{t('Delete record')}}",
- content: "{{t('Are you sure you want to delete it?')}}",
- },
+ role: 'button',
+ isBulk: true,
},
},
create: {
@@ -309,13 +306,12 @@ export const collectionTableSchema: ISchema = {
delete: {
type: 'void',
title: '{{ t("Delete") }}',
- 'x-component': 'Action.Link',
+ 'x-component': 'DeleteCollection',
'x-component-props': {
- confirm: {
- title: "{{t('Delete record')}}",
- content: "{{t('Are you sure you want to delete it?')}}",
- },
- useAction: '{{ cm.useDestroyActionAndRefreshCM }}',
+ role: 'button',
+ 'aria-label': '{{ "delete-button-" + $record.name }}',
+ type: 'primary',
+ className: 'nb-action-link',
},
},
},
diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts
index 187458764..868d8eee4 100644
--- a/packages/core/client/src/locale/en_US.ts
+++ b/packages/core/client/src/locale/en_US.ts
@@ -783,4 +783,6 @@ export default {
Execute: 'Execute',
'Please use a valid SELECT or WITH AS statement': 'Please use a valid SELECT or WITH AS statement',
'Please confirm the SQL statement first': 'Please confirm the SQL statement first',
+ 'Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects':
+ 'Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects',
};
diff --git a/packages/core/client/src/locale/ja_JP.ts b/packages/core/client/src/locale/ja_JP.ts
index c9fb96d5d..4b0b985a8 100644
--- a/packages/core/client/src/locale/ja_JP.ts
+++ b/packages/core/client/src/locale/ja_JP.ts
@@ -668,4 +668,6 @@ export default {
"Search plugin": "プラグインを検索",
"Author": "著者",
"Plugin loading failed. Please check the server logs.": "プラグインのロードに失敗しました。サーバーログを確認してください。",
+ 'Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects':
+ 'テーブルに依存するオブジェクト、およびそれらに依存するオブジェクトを自動的に削除する',
}
diff --git a/packages/core/client/src/locale/pt_BR.ts b/packages/core/client/src/locale/pt_BR.ts
index f0c46cc38..b3dbc6e3d 100644
--- a/packages/core/client/src/locale/pt_BR.ts
+++ b/packages/core/client/src/locale/pt_BR.ts
@@ -710,5 +710,7 @@ export default {
'Form data templates': 'Modelos de dados do formulário',
"Data template": "Modelo de dados",
"Not found":"Não encontrado",
- "Add":"Adicionar"
+ "Add":"Adicionar",
+ 'Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects':
+ 'Excluir automaticamente objetos que dependem desta tabela, bem como objetos que dependem desses objetos',
};
diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts
index 468d46f4f..f69229664 100644
--- a/packages/core/client/src/locale/zh_CN.ts
+++ b/packages/core/client/src/locale/zh_CN.ts
@@ -878,4 +878,6 @@ export default {
Execute: '执行',
'Please use a valid SELECT or WITH AS statement': '请使用有效的 SELECT 或 WITH AS 语句',
'Please confirm the SQL statement first': '请先确认 SQL 语句',
+ 'Automatically drop objects that depend on the collection (such as views), and in turn all objects that depend on those objects':
+ '自动删除依赖于该表的对象,以及依赖这些对象的对象',
};
diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx
index 90964d2c8..deec37c7d 100644
--- a/packages/core/client/src/schema-component/antd/action/Action.tsx
+++ b/packages/core/client/src/schema-component/antd/action/Action.tsx
@@ -2,7 +2,7 @@ import { observer, RecursionField, useField, useFieldSchema, useForm } from '@fo
import { isPortalInBody } from '@nocobase/utils/client';
import { App, Button, Popover } from 'antd';
import classnames from 'classnames';
-import { default as lodash } from 'lodash';
+import { default as lodash, isFunction } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useActionContext } from '../..';
diff --git a/packages/core/database/src/__tests__/collection.test.ts b/packages/core/database/src/__tests__/collection.test.ts
index 0fd65394c..4cedfd156 100644
--- a/packages/core/database/src/__tests__/collection.test.ts
+++ b/packages/core/database/src/__tests__/collection.test.ts
@@ -83,7 +83,6 @@ describe('collection', () => {
});
test('removeFromDb', async () => {
- await db.clean({ drop: true });
const collection = db.collection({
name: 'test',
fields: [
@@ -109,6 +108,30 @@ describe('collection', () => {
expect(r4).toBe(false);
});
+ test('remove from db with cascade', async () => {
+ const testCollection = db.collection({
+ name: 'test',
+ fields: [
+ {
+ type: 'string',
+ name: 'name',
+ },
+ ],
+ });
+
+ await db.sync();
+
+ const viewName = `test_view`;
+ const viewSQL = `create view ${viewName} as select * from ${testCollection.getTableNameWithSchemaAsString()}`;
+ await db.sequelize.query(viewSQL);
+
+ await expect(
+ testCollection.removeFromDb({
+ cascade: true,
+ }),
+ ).resolves.toBeTruthy();
+ });
+
test('collection disable authGenId', async () => {
const Test = db.collection({
name: 'test',
diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts
index 8075a34f9..be736eefc 100644
--- a/packages/core/database/src/collection.ts
+++ b/packages/core/database/src/collection.ts
@@ -383,7 +383,7 @@ export class Collection<
}
remove() {
- this.context.database.removeCollection(this.name);
+ return this.context.database.removeCollection(this.name);
}
async removeFromDb(options?: QueryInterfaceDropTableOptions) {
@@ -396,7 +396,8 @@ export class Collection<
const queryInterface = this.db.sequelize.getQueryInterface();
await queryInterface.dropTable(this.getTableNameWithSchema(), options);
}
- this.remove();
+
+ return this.remove();
}
async existsInDb(options?: Transactionable) {
diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/resources/collections.test.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/resources/collections.test.ts
index 10e74434a..cb1cd8da8 100644
--- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/resources/collections.test.ts
+++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/resources/collections.test.ts
@@ -13,6 +13,31 @@ describe('collections', () => {
await app.destroy();
});
+ test('remove collection with cascade options', async () => {
+ await app
+ .agent()
+ .resource('collections')
+ .create({
+ values: {
+ name: 'test',
+ },
+ });
+ const collection = app.db.getCollection('test');
+ expect(await collection.existsInDb()).toBeTruthy();
+
+ // create a database view for test
+ await app.db.sequelize.query(`
+ CREATE VIEW test_view AS SELECT * FROM ${collection.getTableNameWithSchemaAsString()};
+ `);
+
+ await app.agent().resource('collections').destroy({
+ filterByTk: 'test',
+ cascade: true,
+ });
+
+ expect(await collection.existsInDb()).toBeFalsy();
+ });
+
test('remove collection 1', async () => {
await app
.agent()
diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/models/collection.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/models/collection.ts
index 856634d32..08a3675d2 100644
--- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/models/collection.ts
+++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/models/collection.ts
@@ -1,7 +1,7 @@
import Database, { Collection, MagicAttributeModel, SyncOptions, Transactionable } from '@nocobase/database';
import lodash from 'lodash';
import { FieldModel } from './field';
-import { async } from 'fast-glob';
+import { QueryInterfaceDropTableOptions } from 'sequelize';
interface LoadOptions extends Transactionable {
// TODO
@@ -85,7 +85,7 @@ export class CollectionModel extends MagicAttributeModel {
}
}
- async remove(options?: any) {
+ async remove(options?: Transactionable & QueryInterfaceDropTableOptions) {
const { transaction } = options || {};
const name = this.get('name');
const collection = this.db.getCollection(name);
@@ -109,9 +109,7 @@ export class CollectionModel extends MagicAttributeModel {
}
}
- await collection.removeFromDb({
- transaction,
- });
+ await collection.removeFromDb(options);
}
async migrate(options?: SyncOptions & Transactionable) {
diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts
index ddb49f512..c9c6d6b1a 100644
--- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts
+++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/server.ts
@@ -76,7 +76,18 @@ export class CollectionManagerPlugin extends Plugin {
);
this.app.db.on('collections.beforeDestroy', async (model: CollectionModel, options) => {
- await model.remove(options);
+ const removeOptions = {};
+ if (options.transaction) {
+ removeOptions['transaction'] = options.transaction;
+ }
+
+ const cascade = lodash.get(options, 'context.action.params.cascade', false);
+
+ if (cascade === true || cascade === 'true') {
+ removeOptions['cascade'] = true;
+ }
+
+ await model.remove(removeOptions);
});
// 要在 beforeInitOptions 之前处理
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/action-hooks.tsx b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/action-hooks.tsx
index 466527be8..451b562b6 100644
--- a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/action-hooks.tsx
+++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/action-hooks.tsx
@@ -169,10 +169,13 @@ export const useUpdateCollectionActionAndRefreshCM = () => {
const useDestroyAction = (name) => {
const api = useAPIClient();
+ const form = useForm();
+ const { cascade } = form?.values || {};
return {
async run() {
await api.resource('collections').destroy({
filterByTk: name,
+ cascade,
});
await api.resource('graphPositions').destroy({
filter: { collectionName: name },
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/DeleteCollectionAction.tsx b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/DeleteCollectionAction.tsx
new file mode 100644
index 000000000..783b5878c
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/DeleteCollectionAction.tsx
@@ -0,0 +1,22 @@
+import { DeleteOutlined } from '@ant-design/icons';
+import { DeleteCollection } from '@nocobase/client';
+import React from 'react';
+import { useCancelAction, useUpdateCollectionActionAndRefreshCM } from '../action-hooks';
+import { getPopupContainer } from '../utils';
+
+export const DeleteCollectionAction = ({ item: record, className, ...other }) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx
index 22d9a0d5d..054626c66 100644
--- a/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx
+++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/src/client/components/Entity.tsx
@@ -34,6 +34,7 @@ import { ConnectAssociationAction } from './ConnectAssociationAction';
import { ConnectChildAction } from './ConnectChildAction';
import { ConnectParentAction } from './ConnectParentAction';
import { EditCollectionAction } from './EditCollectionAction';
+import { DeleteCollectionAction } from './DeleteCollectionAction';
import { EditFieldAction } from './EditFieldAction';
import { FieldSummary } from './FieldSummary';
import { OverrideFieldAction } from './OverrideFieldAction';
@@ -436,6 +437,7 @@ const Entity: React.FC<{
components={{
EditOutlined,
EditCollectionAction,
+ DeleteCollectionAction,
ConnectChildAction,
ConnectParentAction,
...options.components,
@@ -475,17 +477,14 @@ const Entity: React.FC<{
delete: {
type: 'void',
'x-action': 'destroy',
- 'x-component': 'Action',
+ 'x-component': 'DeleteCollectionAction',
'x-component-props': {
- component: DeleteOutlined,
- icon: 'DeleteOutlined',
className: 'btn-del',
- confirm: {
- title: "{{t('Delete record')}}",
- getContainer: getPopupContainer,
- collectionConten: "{{t('Are you sure you want to delete it?')}}",
+ getContainer: getPopupContainer,
+ item: collectionData.current,
+ useAction: () => {
+ return useDestroyActionAndRefreshCM({ name, id });
},
- useAction: () => useDestroyActionAndRefreshCM({ name, id }),
},
},
},