feat: 数据表导入导出 (#1550)
Co-authored-by: Toby <2287769986@qq.com> Reviewed-on: daoyoucloud/tachybase#1550
This commit is contained in:
parent
d9d8105b36
commit
1df02b79da
@ -1,8 +1,8 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { ArrayTable } from '@tachybase/components';
|
import { ArrayTable } from '@tachybase/components';
|
||||||
import { ISchema, uid, useField, useForm } from '@tachybase/schema';
|
import { ISchema, uid, useField, useForm } from '@tachybase/schema';
|
||||||
|
|
||||||
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
|
import { DownOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons';
|
||||||
import { Button, Dropdown, MenuProps } from 'antd';
|
import { Button, Dropdown, MenuProps } from 'antd';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -15,6 +15,7 @@ import { useCollectionManager_deprecated } from '../hooks';
|
|||||||
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||||
import * as components from './components';
|
import * as components from './components';
|
||||||
import { TemplateSummary } from './components/TemplateSummary';
|
import { TemplateSummary } from './components/TemplateSummary';
|
||||||
|
import { ImportCollectionMetaAction } from './ImportCollectionMetaAction';
|
||||||
|
|
||||||
const getSchema = (schema, category, compile): ISchema => {
|
const getSchema = (schema, category, compile): ISchema => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
@ -148,6 +149,8 @@ export const AddCollectionAction = (props) => {
|
|||||||
const [schema, setSchema] = useState({});
|
const [schema, setSchema] = useState({});
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const importRef = useRef<any>(null);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const result = [];
|
const result = [];
|
||||||
collectionTemplates.forEach((item) => {
|
collectionTemplates.forEach((item) => {
|
||||||
@ -173,13 +176,19 @@ export const AddCollectionAction = (props) => {
|
|||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
},
|
},
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
|
if (info.key === 'import') {
|
||||||
|
console.log('import', importRef.current);
|
||||||
|
// 打开上传文件的弹窗
|
||||||
|
importRef.current?.showModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const schema = getSchema(getTemplate(info.key), category, compile);
|
const schema = getSchema(getTemplate(info.key), category, compile);
|
||||||
setSchema(schema);
|
setSchema(schema);
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
},
|
},
|
||||||
items,
|
items,
|
||||||
};
|
};
|
||||||
}, [category, items]);
|
}, [category, items, importRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordProvider record={record}>
|
<RecordProvider record={record}>
|
||||||
@ -205,6 +214,7 @@ export const AddCollectionAction = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ActionContextProvider>
|
</ActionContextProvider>
|
||||||
|
<ImportCollectionMetaAction ref={importRef} />
|
||||||
</RecordProvider>
|
</RecordProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,173 @@
|
|||||||
|
import React, { useImperativeHandle, useState } from 'react';
|
||||||
|
import { useField, useForm } from '@tachybase/schema';
|
||||||
|
|
||||||
|
import { InboxOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, message, Modal, Spin, Upload, UploadProps } from 'antd';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useAPIClient } from '../../api-client';
|
||||||
|
import { useRecord } from '../../record-provider';
|
||||||
|
import { useActionContext } from '../../schema-component';
|
||||||
|
import { useCollectionManager_deprecated } from '../hooks';
|
||||||
|
import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider';
|
||||||
|
|
||||||
|
const { Dragger } = Upload;
|
||||||
|
|
||||||
|
function useUploadProps(props: UploadProps): any {
|
||||||
|
const onChange = (param) => {
|
||||||
|
props.onChange?.(param);
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = useAPIClient();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
customRequest({ action, data, file, filename, headers, onError, onProgress, onSuccess, withCredentials }) {
|
||||||
|
const formData = new FormData();
|
||||||
|
if (data) {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
formData.append(key, data[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
formData.append(filename, file);
|
||||||
|
// eslint-disable-next-line promise/catch-or-return
|
||||||
|
api.axios
|
||||||
|
.post(action, formData, {
|
||||||
|
withCredentials,
|
||||||
|
headers,
|
||||||
|
onUploadProgress: ({ total, loaded }) => {
|
||||||
|
onProgress({ percent: Math.round((loaded / total) * 100).toFixed(2) }, file);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ data }) => {
|
||||||
|
onSuccess(data, file);
|
||||||
|
})
|
||||||
|
.catch(onError)
|
||||||
|
.finally(() => {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort() {
|
||||||
|
console.log('upload progress is aborted.');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportUpload = (props: any) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { refreshCM } = useCollectionManager_deprecated();
|
||||||
|
const { close } = props;
|
||||||
|
const { refresh } = useResourceActionContext();
|
||||||
|
|
||||||
|
const uploadProps: UploadProps = {
|
||||||
|
multiple: false,
|
||||||
|
action: '/collections:importMeta',
|
||||||
|
async onChange(info) {
|
||||||
|
if (info.fileList.length > 1) {
|
||||||
|
info.fileList.splice(0, info.fileList.length - 1); // 只保留一个文件
|
||||||
|
}
|
||||||
|
const { status } = info.file;
|
||||||
|
if (status === 'done') {
|
||||||
|
close();
|
||||||
|
message.success(`${info.file.name} ` + t('file uploaded successfully'));
|
||||||
|
refresh();
|
||||||
|
await refreshCM();
|
||||||
|
} else if (status === 'error') {
|
||||||
|
// message.error(`${info.file.name} ` + t('file upload failed'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrop(e) {
|
||||||
|
console.log('Dropped files', e.dataTransfer.files);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dragger {...useUploadProps(uploadProps)}>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text"> {t('Click or drag file to this area to upload')}</p>
|
||||||
|
</Dragger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useCreateCollection = (schema?: any) => {
|
||||||
|
const form = useForm();
|
||||||
|
const { refreshCM } = useCollectionManager_deprecated();
|
||||||
|
const ctx = useActionContext();
|
||||||
|
const { refresh } = useResourceActionContext();
|
||||||
|
const { resource } = useResourceContext();
|
||||||
|
const field = useField();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
field.data = field.data || {};
|
||||||
|
field.data.loading = true;
|
||||||
|
try {
|
||||||
|
await form.submit();
|
||||||
|
const values = cloneDeep(form.values);
|
||||||
|
if (schema?.events?.beforeSubmit) {
|
||||||
|
schema.events.beforeSubmit(values);
|
||||||
|
}
|
||||||
|
if (!values.autoCreateReverseField) {
|
||||||
|
delete values.reverseField;
|
||||||
|
}
|
||||||
|
delete values.autoCreateReverseField;
|
||||||
|
await resource.create({
|
||||||
|
values: {
|
||||||
|
logging: true,
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ctx.setVisible(false);
|
||||||
|
await form.reset();
|
||||||
|
field.data.loading = false;
|
||||||
|
refresh();
|
||||||
|
await refreshCM();
|
||||||
|
} catch (error) {
|
||||||
|
field.data.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImportCollectionMetaAction = React.forwardRef((props, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [dataTypes, setDataTypes] = useState<any[]>(['required']);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
// const [restoreData, setRestoreData] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
showModal,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
// setRestoreData(null);
|
||||||
|
setDataTypes(['required']);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* <ButtonComponent onClick={showModal}>{t('Import')}</ButtonComponent> */}
|
||||||
|
<Modal
|
||||||
|
title={t('Import')}
|
||||||
|
width={800}
|
||||||
|
footer={undefined}
|
||||||
|
open={isModalOpen}
|
||||||
|
onOk={handleCancel}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<ImportUpload close={handleCancel} />
|
||||||
|
</Spin>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -121,7 +121,7 @@ export const SourceKey = observer(
|
|||||||
const field: any = useField();
|
const field: any = useField();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const options = getCollection(collectionName || name)
|
const options = getCollection(collectionName || name)
|
||||||
.fields?.filter((v) => {
|
?.fields?.filter((v) => {
|
||||||
return v.primaryKey || v.unique;
|
return v.primaryKey || v.unique;
|
||||||
})
|
})
|
||||||
.map((k) => {
|
.map((k) => {
|
||||||
|
@ -17,6 +17,7 @@ export * from './components/TemplateSummary';
|
|||||||
export * from './components/CollectionFieldInterfaceTag';
|
export * from './components/CollectionFieldInterfaceTag';
|
||||||
export * from './components/CollectionCategory';
|
export * from './components/CollectionCategory';
|
||||||
export * from './components/CollectionTemplateTag';
|
export * from './components/CollectionTemplateTag';
|
||||||
|
export * from './ImportCollectionMetaAction';
|
||||||
|
|
||||||
registerValidateFormats({
|
registerValidateFormats({
|
||||||
uid: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
|
uid: /^[a-zA-Z][a-zA-Z0-9_-]*$/,
|
||||||
|
@ -48,6 +48,7 @@ import { InheritanceCollectionMixin } from './mixins/InheritanceCollectionMixin'
|
|||||||
import {
|
import {
|
||||||
ExpressionCollectionTemplate,
|
ExpressionCollectionTemplate,
|
||||||
GeneralCollectionTemplate,
|
GeneralCollectionTemplate,
|
||||||
|
ImportCollectionTemplate,
|
||||||
SqlCollectionTemplate,
|
SqlCollectionTemplate,
|
||||||
TreeCollectionTemplate,
|
TreeCollectionTemplate,
|
||||||
ViewCollectionTemplate,
|
ViewCollectionTemplate,
|
||||||
@ -170,6 +171,7 @@ export class CollectionPlugin extends Plugin {
|
|||||||
SqlCollectionTemplate,
|
SqlCollectionTemplate,
|
||||||
TreeCollectionTemplate,
|
TreeCollectionTemplate,
|
||||||
ViewCollectionTemplate,
|
ViewCollectionTemplate,
|
||||||
|
ImportCollectionTemplate,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { CollectionTemplate } from '../../data-source/collection-template/CollectionTemplate';
|
||||||
|
import { getConfigurableProperties } from './properties';
|
||||||
|
|
||||||
|
export class ImportCollectionTemplate extends CollectionTemplate {
|
||||||
|
name = 'import';
|
||||||
|
title = '{{t("Import collection")}}';
|
||||||
|
order = 5;
|
||||||
|
color = 'blue';
|
||||||
|
divider = true;
|
||||||
|
default = {
|
||||||
|
fields: [],
|
||||||
|
};
|
||||||
|
}
|
@ -3,3 +3,4 @@ export * from './tree';
|
|||||||
export * from './expression';
|
export * from './expression';
|
||||||
export * from './view';
|
export * from './view';
|
||||||
export * from './sql';
|
export * from './sql';
|
||||||
|
export * from './import';
|
||||||
|
@ -958,7 +958,8 @@
|
|||||||
"Designer Mode":"设计者模式",
|
"Designer Mode":"设计者模式",
|
||||||
"There are no full screen blocks available at the current location":"当前位置没有可全屏区块",
|
"There are no full screen blocks available at the current location":"当前位置没有可全屏区块",
|
||||||
"Exit Full Screen":"退出全屏",
|
"Exit Full Screen":"退出全屏",
|
||||||
|
"User settings":"个人设置",
|
||||||
|
"Import collection": "导入数据表",
|
||||||
"Embedded page": "嵌入页面",
|
"Embedded page": "嵌入页面",
|
||||||
"Demonstration text": "演示文本",
|
"Demonstration text": "演示文本"
|
||||||
"User settings":"个人设置"
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,137 @@
|
|||||||
|
import fsPromises from 'fs/promises';
|
||||||
|
import os from 'os';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import { Context } from '@tachybase/actions';
|
||||||
|
import { ResourceOptions } from '@tachybase/resourcer';
|
||||||
|
import { koaMulter as multer, uid } from '@tachybase/utils';
|
||||||
|
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
export const collectionImportExportMeta: ResourceOptions = {
|
||||||
|
name: 'collections',
|
||||||
|
middleware: async (ctx, next) => {
|
||||||
|
if (ctx.action.actionName === 'importMeta') {
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: os.tmpdir(),
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
const randomName = Date.now().toString() + Math.random().toString().slice(2); // 随机生成文件名
|
||||||
|
cb(null, randomName);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({ storage }).single('file');
|
||||||
|
return upload(ctx, next);
|
||||||
|
} else {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async exportMeta(ctx: Context, next) {
|
||||||
|
// todo: define property name;
|
||||||
|
const { collectionName } = ctx.action.params;
|
||||||
|
|
||||||
|
const Collections = ctx.db.getCollection('collections');
|
||||||
|
|
||||||
|
const metaList = [];
|
||||||
|
|
||||||
|
async function getAssociationCollection(name: string) {
|
||||||
|
if (metaList.some((item) => item.name === name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const meta = await Collections.repository.findOne({
|
||||||
|
filterByTk: name,
|
||||||
|
context: ctx,
|
||||||
|
appends: ['category', 'fields'],
|
||||||
|
});
|
||||||
|
if (!meta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metaList.push(meta);
|
||||||
|
const fields = meta.dataValues.fields;
|
||||||
|
for (const field of fields) {
|
||||||
|
ctx.logger.info('field.dataValues', field.dataValues);
|
||||||
|
if (field?.dataValues?.options?.target) {
|
||||||
|
await getAssociationCollection(field?.dataValues?.options?.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await getAssociationCollection(collectionName);
|
||||||
|
|
||||||
|
ctx.body = metaList;
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
async importMeta(ctx: Context, next) {
|
||||||
|
const { file } = ctx;
|
||||||
|
if (!file) {
|
||||||
|
throw new Error('file not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let metaList = null;
|
||||||
|
try {
|
||||||
|
metaList = JSON.parse(await fsPromises.readFile(file.path, 'utf8'));
|
||||||
|
} catch (e) {
|
||||||
|
ctx.logger.info(e);
|
||||||
|
throw new Error('file is not a valid json file');
|
||||||
|
}
|
||||||
|
|
||||||
|
let createCount = 0;
|
||||||
|
|
||||||
|
async function importCollection(meta) {
|
||||||
|
const CollectionRepo = ctx.db.getCollection('collections');
|
||||||
|
// 检查表是否存在
|
||||||
|
let collection = await CollectionRepo.repository.findOne({
|
||||||
|
filterByTk: meta.name,
|
||||||
|
context: ctx,
|
||||||
|
});
|
||||||
|
if (collection) {
|
||||||
|
return;
|
||||||
|
throw new Error('collection name already exists, please change the name');
|
||||||
|
}
|
||||||
|
collection = await CollectionRepo.repository.findOne({
|
||||||
|
filter: { key: meta.key },
|
||||||
|
context: ctx,
|
||||||
|
});
|
||||||
|
if (collection) {
|
||||||
|
return;
|
||||||
|
throw new Error('collection key already exists, please change the key');
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate unique key
|
||||||
|
// meta.fields.forEach((field) => {
|
||||||
|
// field.key = uid();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 检查field是否已经存在
|
||||||
|
const fieldRepo = ctx.db.getCollection('fields');
|
||||||
|
const fieldKeys = meta.fields.map((field) => field.key);
|
||||||
|
|
||||||
|
const existFields = await fieldRepo.repository.findOne({
|
||||||
|
filter: { key: { $in: fieldKeys } },
|
||||||
|
context: ctx,
|
||||||
|
});
|
||||||
|
if (existFields) {
|
||||||
|
return;
|
||||||
|
throw new Error('field key already exists, please change the key');
|
||||||
|
}
|
||||||
|
|
||||||
|
await CollectionRepo.repository.create({
|
||||||
|
values: meta,
|
||||||
|
context: ctx,
|
||||||
|
});
|
||||||
|
createCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const meta of metaList) {
|
||||||
|
await importCollection(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createCount) {
|
||||||
|
throw new Error('no collection created, please check the key or name');
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = { count: createCount };
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -8,6 +8,7 @@ import { Mutex } from 'async-mutex';
|
|||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
|
|
||||||
import { CollectionRepository } from '.';
|
import { CollectionRepository } from '.';
|
||||||
|
import { collectionImportExportMeta } from './actions/collectionImportExportMeta';
|
||||||
import {
|
import {
|
||||||
afterCreateForForeignKeyField,
|
afterCreateForForeignKeyField,
|
||||||
afterCreateForReverseField,
|
afterCreateForReverseField,
|
||||||
@ -258,6 +259,8 @@ export class CollectionManagerPlugin extends Plugin {
|
|||||||
|
|
||||||
this.app.acl.allow('collections', 'list', 'loggedIn');
|
this.app.acl.allow('collections', 'list', 'loggedIn');
|
||||||
this.app.acl.allow('collectionCategories', 'list', 'loggedIn');
|
this.app.acl.allow('collectionCategories', 'list', 'loggedIn');
|
||||||
|
|
||||||
|
this.app.resourcer.define(collectionImportExportMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"@tachybase/schema": "workspace:*",
|
"@tachybase/schema": "workspace:*",
|
||||||
"ahooks": "^3.7.2",
|
"ahooks": "^3.7.2",
|
||||||
"antd": "5.19.4",
|
"antd": "5.19.4",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"react-i18next": "^14.1.2",
|
"react-i18next": "^14.1.2",
|
||||||
"react-router-dom": "^6.25.1",
|
"react-router-dom": "^6.25.1",
|
||||||
|
@ -3,11 +3,14 @@ import {
|
|||||||
CollectionTemplateTag,
|
CollectionTemplateTag,
|
||||||
i18n,
|
i18n,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
|
useRecord,
|
||||||
|
useResourceContext,
|
||||||
type CollectionOptions,
|
type CollectionOptions,
|
||||||
} from '@tachybase/client';
|
} from '@tachybase/client';
|
||||||
import { ISchema, Schema, uid } from '@tachybase/schema';
|
import { ISchema, Schema, uid } from '@tachybase/schema';
|
||||||
|
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const compile = (source) => {
|
const compile = (source) => {
|
||||||
@ -305,6 +308,29 @@ export const collectionTableSchema: ISchema = {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
export: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Export") }}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction() {
|
||||||
|
const { resource, targetKey } = useResourceContext();
|
||||||
|
const { [targetKey]: collectionName } = useRecord();
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
const { data } = await resource.exportMeta(
|
||||||
|
{ collectionName },
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const blob = new Blob([JSON.stringify(data.data, null, 2)], { type: 'application/json' });
|
||||||
|
saveAs(blob, `${collectionName}.json`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
delete: {
|
delete: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Delete") }}',
|
title: '{{ t("Delete") }}',
|
||||||
|
@ -2584,6 +2584,9 @@ importers:
|
|||||||
antd:
|
antd:
|
||||||
specifier: 5.19.4
|
specifier: 5.19.4
|
||||||
version: 5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 5.19.4(date-fns@3.6.0)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
file-saver:
|
||||||
|
specifier: ^2.0.5
|
||||||
|
version: 2.0.5
|
||||||
lodash:
|
lodash:
|
||||||
specifier: 4.17.21
|
specifier: 4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
Loading…
Reference in New Issue
Block a user