feat: plugin export (#479)

* feat: init export plugin

* feat: add client export

* fix: fix the word spell

* feat: export plugin done

* feat: init export plugin

* feat: add client export

* fix: fix the word spell

* feat: export plugin done

* ci: change plugin-export version

* refactor: renders add ctx params

* fix: fix select and multipleSelect export

* fix: array convert string

* refactor: move SchemaInitializerPluginProvider

* fix: build error

* fix: change umijs config

* fix: update SchemaInitializerPluginProvider

* fix: import server

* fix: fix some bug

* fix: fix some bug

* refactor: export plugin refactor

* refactor: create all export fields by default

* fix: fix export plugin bug

* fix(plugin-collection-manager): uiSchema toJSON

* fix: update yarn.lock

* fix: fix init fields bug

* refactor: enum params pass by client

* fix: fix export table header title

* refactor: refactor dataIndex

* fix: fix dataIndex maybe complex object

* fix: add checkboxGroup in export plugin

* fix: add checkbox and i18n

* feat: improve code

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
SemmyWong 2022-06-14 15:01:53 +08:00 committed by GitHub
parent e69257e346
commit d831a9b889
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1124 additions and 167 deletions

View File

@ -3,7 +3,8 @@
"version": "0.7.0-alpha.83",
"license": "MIT",
"devDependencies": {
"@nocobase/client": "0.7.0-alpha.83"
"@nocobase/client": "0.7.0-alpha.83",
"@nocobase/plugin-export": "0.7.0-alpha.83"
},
"repository": {
"type": "git",

View File

@ -36,6 +36,7 @@ import {
WorkflowShortcut
} from '@nocobase/client';
import { AuditLogsProvider } from '@nocobase/plugin-audit-logs/client';
import { ExportPluginProvider } from '@nocobase/plugin-export/client';
import { notification } from 'antd';
import 'antd/dist/antd.css';
import React from 'react';
@ -105,6 +106,7 @@ const providers = [
},
},
],
ExportPluginProvider,
AuditLogsProvider,
BlockSchemaComponentProvider,
AntdSchemaComponentProvider,

View File

@ -18,22 +18,6 @@
},
"allowSyntheticDefaultImports": true
},
"include": [
"mock/**/*",
"src/**/*",
"config/**/*",
".umirc.ts",
"typings.d.ts"
],
"exclude": [
"node_modules",
"lib",
"es",
"dist",
"typings",
"**/__test__",
"test",
"docs",
"tests"
]
"include": ["mock/**/*", "src/**/*", "config/**/*", ".umirc.ts", "typings.d.ts"],
"exclude": ["node_modules", "lib", "es", "dist", "typings", "**/__test__", "test", "docs", "tests"]
}

View File

@ -12,5 +12,6 @@ export default [
'@nocobase/plugin-china-region',
'@nocobase/plugin-workflow',
'@nocobase/plugin-client',
'@nocobase/plugin-export',
'@nocobase/plugin-audit-logs',
] as PluginsConfigurations;

View File

@ -57,7 +57,6 @@ async function listWithNonPaged(ctx: Context) {
export async function list(ctx: Context, next) {
const { paginate } = ctx.action.params;
if (paginate === false || paginate === 'false') {
await listWithNonPaged(ctx);
} else {

View File

@ -16,7 +16,7 @@ export const useBlockResource = () => {
return useContext(BlockResourceContext);
};
interface UseReousrceProps {
interface UseResourceProps {
resource: any;
association?: any;
useSourceId?: any;
@ -33,7 +33,7 @@ const useAssociation = (props) => {
}
};
const useReousrce = (props: UseReousrceProps) => {
const useResource = (props: UseResourceProps) => {
const { block, resource, useSourceId } = props;
const record = useRecord();
const api = useAPIClient();
@ -121,7 +121,7 @@ export const useBlockRequestContext = () => {
export const BlockProvider = (props) => {
const { collection, association } = props;
const resource = useReousrce(props);
const resource = useResource(props);
return (
<MaybeCollectionProvider collection={collection}>
<BlockAssociationContext.Provider value={association}>

View File

@ -4,6 +4,17 @@ import { CollectionManagerContext } from '../context';
export const useCollectionManager = () => {
const { refreshCM, service, interfaces, collections } = useContext(CollectionManagerContext);
const getCollectionField = (name: string) => {
const [collectionName, fieldName] = name.split('.');
if (!fieldName) {
return;
}
const collection = collections?.find((collection) => collection.name === collectionName);
if (!collection) {
return;
}
return collection?.fields?.find((field) => field.name === fieldName);
};
return {
service,
interfaces,
@ -22,16 +33,19 @@ export const useCollectionManager = () => {
const collection = collections?.find((collection) => collection.name === name);
return collection?.fields || [];
},
getCollectionField(name: string) {
const [collectionName, fieldName] = name.split('.');
if (!fieldName) {
getCollectionField,
getCollectionJoinField(name: string) {
const [collectionName, ...fieldNames] = name.split('.');
if (!fieldNames?.length) {
return;
}
const collection = collections?.find((collection) => collection.name === collectionName);
if (!collection) {
return;
}
return collection?.fields?.find((field) => field.name === fieldName);
let cName = collectionName;
return fieldNames.reduce((result, curFieldName) => {
const collectionField = getCollectionField(`${cName}.${curFieldName}`);
cName = collectionField.target;
return collectionField;
}, null);
},
getInterface(name: string) {
return interfaces[name] ? clone(interfaces[name]) : null;

View File

@ -26,4 +26,3 @@ export * from './slate';
export * from './system-settings';
export * from './user';
export * from './workflow';

View File

@ -251,7 +251,7 @@ export default {
"Title": "Title",
"Select view": "Select view",
"Reset": "Reset",
"Export fields": "Export fields",
"Exportable fields": "Exportable fields",
"Saved successfully": "Saved successfully",
"Nickname": "Nickname",
"Sign in": "Sign in",
@ -532,8 +532,7 @@ export default {
"Request success": "Request success",
"Invalid JSON format": "Invalid JSON format",
"After successful request": "After successful request",
"Add exported field": "Add exported field",
"Exported table header name": "Exported table header name",
"Add exportable field": "Add exportable field",
"Audit logs": "Audit logs",
"Record ID": "Record ID",
"User": "User",

View File

@ -262,7 +262,7 @@ export default {
"Title": "标题",
"Select view": "切换视图",
"Reset": "重置",
"Export fields": "导出字段",
"Exportable fields": "导出字段",
"Saved successfully": "保存成功",
"Nickname": "昵称",
"Sign in": "登录",
@ -584,6 +584,8 @@ export default {
'Request success': '请求成功',
'Invalid JSON format': '非法JSON格式',
'After successful request': '请求成功之后',
'Add exportable field': '添加可导出字段',
// 'Custom column title': '自定义列标题',
"Audit logs": "操作记录",
"Record ID": "数据 ID",
"User": "用户",

View File

@ -73,7 +73,7 @@ export const Cascader = connect(
fieldNames={fieldNames}
displayRender={displayRender}
onChange={(value, selectedOptions) => {
if (labelInValue) {
if (value && labelInValue) {
onChange(selectedOptions.map((option) => omit(option, [fieldNames.children])));
} else {
onChange(value);

View File

@ -15,7 +15,6 @@ export interface SchemaInitializerProviderProps {
export const useSchemaInitializer = (name: string) => {
const initializers = useContext(SchemaInitializerContext);
const render = (component?: any, props?: any) => {
return component && React.createElement(component, props);
};
@ -46,6 +45,8 @@ export const useSchemaInitializer = (name: string) => {
};
};
export const SchemaInitializerPluginContext = createContext(null);
export const SchemaInitializerProvider: React.FC<SchemaInitializerProviderProps> = (props) => {
const { initializers, components, children } = props;
return (

View File

@ -476,10 +476,12 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
const { dn } = useSchemaSettings();
const compile = useCompile();
const api = useAPIClient();
const form = useMemo(
() =>
createForm({
initialValues: cloneDeep(initialValues),
values: cloneDeep(initialValues),
}),
[],
);
@ -508,7 +510,7 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
return (
<>
<SchemaSettings.Item {...others} onClick={openAssignedFieldValueHandler}>
<SchemaSettings.Item {...others} onClick={openAssignedFieldValueHandler} onKeyDown={(e) => e.stopPropagation()}>
{props.children || props.title}
</SchemaSettings.Item>
{createPortal(

View File

@ -3,10 +3,12 @@ export declare function getUmiConfig(): {
'process.env.API_BASE_URL': string;
};
proxy: {
[x: string]: {
[x: string]:
| {
target: string;
changeOrigin: boolean;
} | {
}
| {
target: string;
changeOrigin: boolean;
pathRewrite: {

View File

@ -1,7 +1,7 @@
import _ from 'lodash';
import Resourcer from './resourcer';
import Middleware, { MiddlewareType } from './middleware';
import Action, { ActionName, ActionType } from './action';
import Middleware, { MiddlewareType } from './middleware';
import Resourcer from './resourcer';
export type ResourceType = 'single' | 'hasOne' | 'hasMany' | 'belongsTo' | 'belongsToMany';

View File

@ -1 +1 @@
export { default } from './plugin';
export { default } from './server';

View File

@ -1,5 +1,5 @@
import { mockServer } from '@nocobase/test';
import CollectionManagerPlugin from '../plugin';
import CollectionManagerPlugin from '../server';
describe('collections repository', () => {
it('case 1', async () => {

View File

@ -1,3 +1,3 @@
export { default } from './plugin';
export * from './repositories';
export { default } from './server';

View File

@ -28,7 +28,7 @@ export class FieldModel extends MagicAttributeModel {
const uiSchema = await UISchema.findByPk(options.uiSchemaUid, {
transaction: loadOptions.transaction,
});
return collection.setField(name, { ...options, uiSchema });
return collection.setField(name, { ...options, uiSchema: uiSchema.get() });
} else {
return collection.setField(name, options);
}

2
packages/plugins/export/client.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// @ts-nocheck
export * from './lib/client';

View File

@ -0,0 +1,12 @@
var _useExportClient = require("./lib/client");
Object.keys(_useExportClient).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _useExportClient[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _useExportClient[key];
}
});
});

View File

@ -0,0 +1,18 @@
{
"name": "@nocobase/plugin-export",
"version": "0.7.0-alpha.83",
"main": "lib/server/index.js",
"dependencies": {
"node-xlsx": "^0.16.1",
"@nocobase/client": "0.7.0-alpha.83",
"@nocobase/server": "0.7.0-alpha.83"
},
"devDependencies": {
"@types/node-xlsx": "^0.15.1"
},
"peerDependencies": {
"@nocobase/server": "*",
"@nocobase/client": "*",
"@nocobase/test": "*"
}
}

2
packages/plugins/export/server.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
// @ts-nocheck
export * from './lib/server';

View File

@ -0,0 +1,12 @@
var _useExportServer = require("./lib/server");
Object.keys(_useExportServer).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (key in exports && exports[key] === _useExportServer[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _useExportServer[key];
}
});
});

View File

@ -0,0 +1,94 @@
import { Schema, useFieldSchema } from '@formily/react';
import { merge } from '@formily/shared';
import { SchemaInitializer, useCollection, useCompile, useDesignable } from '@nocobase/client';
import { cloneDeep } from 'lodash';
import React from 'react';
import { useFields } from './useFields';
const findSchema = (schema: Schema, key: string, action: string) => {
return schema.reduceProperties((buf, s) => {
if (s[key] === action) {
return s;
}
const c = findSchema(s, key, action);
if (c) {
return c;
}
return buf;
});
};
const removeSchema = (schema, cb) => {
return cb(schema);
};
export const useCurrentSchema = (action: string, key: string, find = findSchema, rm = removeSchema) => {
const fieldSchema = useFieldSchema();
const { remove } = useDesignable();
const schema = find(fieldSchema, key, action);
return {
schema,
exists: !!schema,
remove() {
schema && rm(schema, remove);
},
};
};
const initExportSettings = (fields) => {
const exportSettings = [];
const generateDataIndex = (di, preFields, fNodes: any[]) => {
let child = cloneDeep(preFields);
fNodes.reduce((buf, cur) => {
if (cur.children) {
const childDI = [];
preFields.dataIndex.push(cur.name);
generateDataIndex(childDI, cloneDeep(preFields), cur.children);
preFields.dataIndex.pop();
di.push(...childDI);
} else {
child.dataIndex.push(cur.name);
di.push(child);
child = cloneDeep(preFields);
}
}, []);
};
generateDataIndex(exportSettings, { dataIndex: [] }, fields);
return exportSettings;
};
export const ExportActionInitializer = (props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentSchema('export', 'x-action', item.find, item.remove);
const compile = useCompile();
const { name } = useCollection();
const fields = useFields(name);
const schema = {
type: 'void',
title: '{{ t("Export") }}',
'x-action': 'export',
'x-action-settings': {
exportSettings: [],
},
'x-designer': 'ExportDesigner',
'x-component': 'Action',
'x-component-props': {
icon: 'clouddownloadoutlined',
useProps: '{{ useExportAction }}',
},
};
return (
<SchemaInitializer.SwitchItem
checked={exists}
title={item.title}
onClick={() => {
if (exists) {
return remove();
}
schema['x-action-settings']['exportSettings'] = initExportSettings(fields);
const s = merge(schema || {}, item.schema || {});
item?.schemaInitialize?.(s);
insert(s);
}}
/>
);
};

View File

@ -0,0 +1,122 @@
import { ArrayItems } from '@formily/antd';
import type { ISchema } from '@formily/react';
import { useField, useFieldSchema } from '@formily/react';
import { GeneralSchemaDesigner, SchemaSettings, useDesignable } from '@nocobase/client';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useShared } from './useShared';
export const ExportDesigner = () => {
const field = useField();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
const [schema, setSchema] = useState<ISchema>();
const { schema: pageSchema } = useShared();
useEffect(() => {
setSchema(pageSchema);
}, [field.address, fieldSchema?.['x-action-settings']?.['exportSettings']]);
return (
<GeneralSchemaDesigner disableInitializer>
<SchemaSettings.ModalItem
title={t('Edit button')}
schema={
{
type: 'object',
title: t('Edit button'),
properties: {
title: {
'x-decorator': 'FormItem',
'x-component': 'Input',
title: t('Button title'),
default: fieldSchema.title,
'x-component-props': {},
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
},
icon: {
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
title: t('Button icon'),
default: fieldSchema?.['x-component-props']?.icon,
'x-component-props': {},
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
},
type: {
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
title: t('Button background color'),
default: fieldSchema?.['x-component-props']?.danger
? 'danger'
: fieldSchema?.['x-component-props']?.type === 'primary'
? 'primary'
: 'default',
enum: [
{ value: 'default', label: '{{t("Default")}}' },
{ value: 'primary', label: '{{t("Highlight")}}' },
{ value: 'danger', label: '{{t("Danger red")}}' },
],
},
},
} as ISchema
}
onSubmit={({ title, icon, type }) => {
if (title) {
fieldSchema.title = title;
field.title = title;
field.componentProps.icon = icon;
field.componentProps.danger = type === 'danger';
field.componentProps.type = type;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].icon = icon;
fieldSchema['x-component-props'].danger = type === 'danger';
fieldSchema['x-component-props'].type = type;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
title,
'x-component-props': {
...fieldSchema['x-component-props'],
},
},
});
dn.refresh();
}
}}
/>
<SchemaSettings.ActionModalItem
title={t('Exportable fields')}
schema={schema}
initialValues={{ exportSettings: fieldSchema?.['x-action-settings']?.exportSettings }}
components={{ ArrayItems }}
onSubmit={({ exportSettings }) => {
fieldSchema['x-action-settings']['exportSettings'] = exportSettings
?.filter((fieldItem) => fieldItem?.dataIndex?.length)
.map((item) => ({
dataIndex: item.dataIndex.map((di) => di.name ?? di),
title: item.title,
}));
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-action-settings': fieldSchema['x-action-settings'],
},
});
dn.refresh();
}}
/>
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={(s) => {
return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar');
}}
confirm={{
title: t('Delete action'),
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -0,0 +1,23 @@
import { SchemaInitializerContext } from '@nocobase/client';
import { useContext } from 'react';
export const ExportInitializerProvider = (props: any) => {
const initializes = useContext(SchemaInitializerContext);
const hasExportAction = initializes.TableActionInitializers.items[0].children.some(
(initialize) => initialize.component === 'ExportActionInitializer',
);
!hasExportAction &&
initializes.TableActionInitializers.items[0].children.push({
type: 'item',
title: "{{t('Export')}}",
component: 'ExportActionInitializer',
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action-props': {
skipScopeCheck: true,
},
},
});
return props.children;
};

View File

@ -0,0 +1,11 @@
import { SchemaComponentOptions } from '@nocobase/client';
import React from 'react';
import { ExportActionInitializer, ExportDesigner, ExportInitializerProvider, useExportAction } from './';
export const ExportPluginProvider = (props: any) => {
return (
<SchemaComponentOptions components={{ ExportActionInitializer, ExportDesigner }} scope={{ useExportAction }}>
<ExportInitializerProvider>{props.children}</ExportInitializerProvider>
</SchemaComponentOptions>
);
};

View File

@ -0,0 +1,6 @@
export * from './ExportActionInitializer';
export * from './ExportDesigner';
export * from './ExportInitializerProvider';
export * from './ExportPluginProvider';
export * from './useExportAction';

View File

@ -0,0 +1,56 @@
import { useFieldSchema } from '@formily/react';
import {
useAPIClient,
useBlockRequestContext,
useCollection,
useCollectionManager,
useCompile,
} from '@nocobase/client';
import { useTranslation } from 'react-i18next';
export const useExportAction = () => {
const { service } = useBlockRequestContext();
const apiClient = useAPIClient();
const actionSchema = useFieldSchema();
const compile = useCompile();
const { getCollectionJoinField } = useCollectionManager();
const { name, title, getField } = useCollection();
const { t } = useTranslation();
return {
async onClick() {
const { exportSettings } = actionSchema?.['x-action-settings'] ?? {};
exportSettings.forEach((es) => {
const { uiSchema } = getCollectionJoinField(`${name}.${es.dataIndex.join('.')}`) ?? {};
es.enum = uiSchema?.enum?.map((e) => ({ value: e.value, label: e.label }));
if (!es.enum && uiSchema.type === 'boolean') {
es.enum = [
{ value: true, label: t('Yes') },
{ value: false, label: t('No') },
];
}
es.defaultTitle = uiSchema?.title;
});
const { data } = await apiClient.request({
url: `/${name}:exportXlsx`,
method: 'get',
responseType: 'blob',
params: {
title: compile(title),
columns: JSON.stringify(compile(exportSettings)),
appends: service.params[0]?.appends?.join(),
filter: JSON.stringify(service.params[0]?.filter),
},
});
let blob = new Blob([data], { type: 'application/x-xls' });
const a = document.createElement('a');
const blobUrl = window.URL.createObjectURL(blob);
a.download = `${compile(title)}.xlsx`;
a.href = blobUrl;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(blobUrl);
document.body.removeChild(a);
},
};
};

View File

@ -0,0 +1,58 @@
import { useFieldSchema } from '@formily/react';
import { useCollectionManager } from '@nocobase/client';
export const useFields = (collectionName: string) => {
const fieldSchema = useFieldSchema();
const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || [];
const { getCollectionFields, getInterface } = useCollectionManager();
const fields = getCollectionFields(collectionName);
const field2option = (field, depth) => {
if (nonfilterable.length && depth === 1 && nonfilterable.includes(field.name)) {
return;
}
if (!field.interface) {
return;
}
const fieldInterface = getInterface(field.interface);
if (!fieldInterface.filterable) {
return;
}
const { nested, children, operators } = fieldInterface.filterable;
const option = {
name: field.name,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field);
}) || [],
};
if (field.target && depth > 2) {
return;
}
if (depth > 2) {
return option;
}
if (children?.length) {
option['children'] = children;
}
if (nested) {
const targetFields = getCollectionFields(field.target);
const options = getOptions(targetFields, depth + 1).filter(Boolean);
option['children'] = option['children'] || [];
option['children'].push(...options);
}
return option;
};
const getOptions = (fields, depth) => {
const options = [];
fields.forEach((field) => {
const option = field2option(field, depth);
if (option) {
options.push(option);
}
});
return options;
};
return getOptions(fields, 1);
};

View File

@ -0,0 +1,92 @@
import { css } from '@emotion/css';
import { useCollection } from '@nocobase/client';
import { useFields } from './useFields';
export const useShared = () => {
const { name } = useCollection();
const fields = useFields(name);
return {
schema: {
type: 'void',
'x-component': 'Grid',
properties: {
exportSettings: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
className: css`
width: 100%;
& .ant-space-item:nth-child(2),
& .ant-space-item:nth-child(3) {
flex: 1;
}
`,
},
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
dataIndex: {
type: 'array',
'x-decorator': 'FormItem',
'x-component': 'Cascader',
required: true,
enum: fields,
'x-component-props': {
fieldNames: {
label: 'title',
value: 'name',
children: 'children',
},
// labelInValue: true,
changeOnSelect: false,
},
},
title: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '{{ t("Custom column title") }}',
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add exportable field") }}',
'x-component': 'ArrayItems.Addition',
'x-component-props': {
className: css`
border-color: rgb(241, 139, 98);
color: rgb(241, 139, 98);
&.ant-btn-dashed:hover {
border-color: rgb(241, 139, 98);
color: rgb(241, 139, 98);
}
`,
},
},
},
},
},
},
};
};

View File

@ -0,0 +1 @@
export { default } from './server';

View File

@ -0,0 +1,51 @@
import { Context, Next } from '@nocobase/actions';
import { Collection, Repository } from '@nocobase/database';
import xlsx from 'node-xlsx';
import render from '../renders';
export async function exportXlsx(ctx: Context, next: Next) {
let { title, columns, associatedName, associatedIndex, resourceName, filter, fields, appends, except } =
ctx.action.params;
if (typeof columns === 'string') {
columns = JSON.parse(columns);
}
columns = columns?.filter((col) => col?.dataIndex?.length > 0);
let repository: Repository;
let collection: Collection;
if (associatedName && associatedIndex) {
const associated = ctx.db.getCollection(associatedName);
const resourceField = associated.getField(resourceName);
collection = ctx.db.getCollection(resourceField.target);
repository = associated.repository.relation(resourceName).of(associatedIndex) as any;
} else {
collection = ctx.db.getCollection(resourceName);
repository = collection.repository;
}
const data = await repository.find({
filter,
fields,
appends,
except,
});
const collectionFields = columns.map((col) => collection.fields.get(col.dataIndex[0]));
const { rows, ranges } = await render({ columns, fields: collectionFields, data }, ctx);
ctx.body = xlsx.build([
{
name: title,
data: rows,
options: {
'!merges': ranges,
},
},
]);
ctx.set({
'Content-Type': 'application/octet-stream',
// to avoid "invalid character" error in header (RFC)
'Content-Disposition': `attachment; filename=${encodeURI(title)}.xlsx`,
});
await next();
}

View File

@ -0,0 +1 @@
export * from './export-xlsx';

View File

@ -0,0 +1,26 @@
import { InstallOptions, Plugin } from '@nocobase/server';
import { exportXlsx } from './actions';
export class ExportPlugin extends Plugin {
getName(): string {
return this.getPackageName(__dirname);
}
beforeLoad() {}
async load() {
// Visit: http://localhost:13000/api/xxx:exportXlsx
this.app.resourcer.registerActionHandler('exportXlsx', exportXlsx);
// this.app.resource({
// name: 'export',
// actions: {
// xlsx: exportXlsx,
// },
// });
this.app.acl.allow('*', 'exportXlsx');
}
async install(options: InstallOptions) {}
}
export default ExportPlugin;

View File

@ -0,0 +1,173 @@
import * as renders from './renders';
function getInterfaceRender(name: string): Function {
return renders[name] || renders._;
}
function renderHeader(params, ctx) {
const { columns, fields, headers = [], rowIndex = 0 } = params;
let { colIndex = 0 } = params;
if (!headers[rowIndex]) {
headers.push([]);
}
const row = headers[rowIndex];
fields.forEach((field, i) => {
const nextColIndex = colIndex + i;
row.push({
column: columns[i],
field,
rowIndex,
colIndex: nextColIndex,
});
// if (field.interface === 'subTable') {
// const subTable = ctx.db.getTable(field.target);
// const subFields = subTable.getOptions().fields.filter((field) => Boolean(field.__index));
// renderHeader(
// {
// fields: subFields,
// headers,
// rowIndex: rowIndex + 1,
// colIndex: nextColIndex,
// },
// ctx,
// );
// colIndex += subFields.length;
// }
});
Object.assign(params, { headers });
}
async function renderRows({ columns, fields, data }, ctx) {
return await data.reduce(async (preResult, row) => {
const result = await preResult;
const thisRow = [];
const rowIndex = 0;
let colOffset = 0;
for (let i = 0, iLen = fields.length; i < iLen; i++) {
const field = fields[i];
if (!thisRow[rowIndex]) {
thisRow.push([]);
}
const cells = thisRow[rowIndex];
if (field.options.interface !== 'subTable') {
const render = getInterfaceRender(field.options.interface);
const value = await render(field, row, ctx, columns[i]);
cells.push({
value,
rowIndex: result.length + rowIndex,
colIndex: i + colOffset,
});
} else {
const subTable = ctx.db.getTable(field.target);
const subFields = subTable.getOptions().fields.filter((item) => Boolean(item.__index));
//TODO: must provide sub-table columns
const subTableColumns = [];
const subRows = await renderRows(
{ columns: subTableColumns, fields: subFields, data: row.get(field.name) || [] },
ctx,
);
// const { rows: subRowGroups } = subTableRows;
subRows.forEach((cells, j) => {
const subRowIndex = rowIndex + j;
if (!thisRow[subRowIndex]) {
thisRow.push([]);
}
const subCells = thisRow[subRowIndex];
subCells.push(
...cells.map((cell) => ({
...cell,
rowIndex: result.length + subRowIndex,
colIndex: cell.colIndex + i,
})),
);
});
colOffset += subFields.length;
}
}
thisRow.forEach((cells) => {
cells.forEach((cell) => {
const relRowIndex = cell.rowIndex - result.length;
Object.assign(cell, {
rowSpan:
relRowIndex >= thisRow.length - 1 ||
thisRow[relRowIndex + 1].find((item) => item.colIndex === cell.colIndex)
? 1
: thisRow.length - relRowIndex,
});
});
});
return result.concat(thisRow);
}, Promise.resolve([]));
}
export default async function ({ columns, fields, data }, ctx) {
const headers = [];
renderHeader({ columns, fields, headers }, ctx);
const ranges = [];
// 计算全表最大的列索引(由于无论如何最大列都是单个单元格,所以等价于长度)
const maxColIndex = Math.max(...headers.map((row) => row[row.length - 1].colIndex));
// 遍历所有单元格,计算需要合并的坐标范围
headers.forEach((row, rowIndex) => {
row.forEach((cell, cellIndex) => {
// 跨行合并的行数为
cell.rowSpan =
cell.rowIndex >= headers.length - 1 ||
headers[cell.rowIndex + 1].find((item) => item.colIndex === cell.colIndex)
? 1
: headers.length - cell.rowIndex;
const nextCell = headers
.slice(0, rowIndex + 1)
.map((r) => r.find((item) => item.colIndex > cell.colIndex))
.filter((c) => Boolean(c))
.reduce((min, c) => (min && Math.min(min.colIndex, c.colIndex) === min.colIndex ? min : c), null);
cell.colSpan = nextCell ? nextCell.colIndex - cell.colIndex : maxColIndex - cell.colIndex + 1;
if (cell.rowSpan > 1 || cell.colSpan > 1) {
ranges.push({
s: { c: cell.colIndex, r: cell.rowIndex },
e: { c: cell.colIndex + cell.colSpan - 1, r: cell.rowIndex + cell.rowSpan - 1 },
});
}
});
});
const rows = (await renderRows({ columns, fields, data }, ctx)).map((row) => {
const cells = Array(maxColIndex).fill(null);
row.forEach((cell) => {
cells.splice(cell.colIndex, 1, cell.value);
if (cell.rowSpan > 1) {
ranges.push({
s: { c: cell.colIndex, r: cell.rowIndex + headers.length },
e: { c: cell.colIndex, r: cell.rowIndex + cell.rowSpan - 1 + headers.length },
});
}
});
return cells;
});
return {
rows: [
...headers.map((row) => {
// 补齐无数据单元格,以供合并
const cells = Array(maxColIndex).fill(null);
row.forEach((cell) =>
cells.splice(
cell.colIndex,
1,
cell.column.title ?? cell.column.defaultTitle ?? cell.column.dataIndex[cell.column.dataIndex.length - 1],
),
);
return cells;
}),
...rows,
],
ranges,
};
}

View File

@ -0,0 +1,98 @@
import moment from 'moment';
export async function _(field, row, ctx, column?: any) {
if (column?.dataIndex.length > 1) {
return column.dataIndex.reduce((result, col) => {
return result?.[col];
}, row);
} else {
return row.get(field.name);
}
}
export async function datetime(field, row, ctx) {
const value = row.get(field.name);
return moment(value).format(field.showTime ? `${field.dateFormat} ${field.timeFormat}` : field.dateFormat);
}
export async function percent(field, row, ctx) {
const value = row.get(field.name);
return value && `${value}%`;
}
export async function boolean(field, row, ctx, column?: any) {
const value = row.get(field.name);
let { enum: enumData } = column ?? {};
if (enumData?.length > 0) {
const option = enumData.find((item) => item.value === value);
return option?.label;
} else {
// FIXME: i18n
return value ? '是' : value === null || value === undefined ? '' : '否';
}
}
export const checkbox = boolean;
export async function select(field, row, ctx, column?: any) {
const value = row.get(field.name);
let { enum: enumData } = column ?? {};
if (!enumData) {
const repository = ctx.db.getCollection('uiSchemas').repository;
const model = await repository.findById(field.options.uiSchemaUid);
enumData = model.get('enum');
}
const option = enumData.find((item) => item.value === value);
return option?.label;
}
export async function multipleSelect(field, row, ctx, column?: any) {
const values = row.get(field.name);
let { enum: enumData } = column ?? {};
if (!enumData) {
const repository = ctx.db.getCollection('uiSchemas').repository;
const model = await repository.findById(field.options.uiSchemaUid);
enumData = model.get('enum');
}
return values
?.map((value) => {
const option = enumData.find((item) => item.value === value);
return option?.label;
})
?.join();
}
export const radio = select;
export const radioGroup = select;
export const checkboxes = multipleSelect;
export const checkboxGroup = multipleSelect;
export async function subTable(field, row, ctx) {
// TODO: need title field to be defined
return (row.get(field.name) || []).map((item) => item[field.sourceKey]);
}
export async function linkTo(field, row, ctx, column?: any) {
return (row.get(field.name) || []).map((item) => {
return column.dataIndex.reduce((buf, cur) => {
buf = item[cur];
return buf;
});
});
// return (row.get(field.name) || []).map((item) => item[field.labelField]);
}
export async function attachment(field, row, ctx) {
return (row.get(field.name) || []).map((item) => item[field.url]).join(' ');
}
export async function chinaRegion(field, row, ctx) {
const value = row.get(field.name);
const values = (Array.isArray(value) ? value : [value]).sort((a, b) =>
a.level !== b.level ? a.level - b.level : a.sort - b.sort,
);
return values.map((item) => item.name).join('/');
}

View File

@ -1 +1 @@
export { default } from './plugin';
export { default } from './server';

View File

@ -1,105 +1 @@
import { skip } from '@nocobase/acl';
import { MagicAttributeModel } from '@nocobase/database';
import { Plugin } from '@nocobase/server';
import { resolve } from 'path';
import { getAccessible } from './actions/getAccessible';
export class UiRoutesStoragePlugin extends Plugin {
getName(): string {
return this.getPackageName(__dirname);
}
async install() {
const repository = this.app.db.getRepository('uiRoutes');
const routes = [
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
{
type: 'route',
uiSchema: {
type: 'void',
'x-component': 'Menu',
'x-designer': 'Menu.Designer',
'x-initializer': 'MenuItemInitializers',
'x-component-props': {
mode: 'mix',
theme: 'dark',
// defaultSelectedUid: 'u8',
onSelect: '{{ onSelect }}',
sideMenuRefScopeKey: 'sideMenuRef',
},
properties: {},
},
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: 'NocoBase Admin',
routes: [
// test...
// {
// type: 'route',
// path: '/admin/workflows/:id',
// component: 'WorkflowPage',
// },
// {
// type: 'route',
// path: '/admin/block-templates/:key',
// component: 'BlockTemplateDetails',
// },
// {
// type: 'route',
// path: '/admin/block-templates',
// component: 'BlockTemplatePage',
// },
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'RouteSchemaComponent',
},
],
},
{
type: 'route',
component: 'AuthLayout',
routes: [
{
type: 'route',
path: '/signin',
component: 'SigninPage',
},
{
type: 'route',
path: '/signup',
component: 'SignupPage',
},
],
},
];
for (const values of routes) {
await repository.create({
values,
});
}
}
async load() {
this.app.resourcer.registerActionHandler('uiRoutes:getAccessible', getAccessible);
this.app.db.registerModels({ MagicAttributeModel });
await this.app.db.import({
directory: resolve(__dirname, 'collections'),
});
this.app.acl.use(
skip({
resourceName: 'uiRoutes',
actionName: 'getAccessible',
}),
);
}
}
export default UiRoutesStoragePlugin;
export { default } from './server';

View File

@ -0,0 +1,105 @@
import { skip } from '@nocobase/acl';
import { MagicAttributeModel } from '@nocobase/database';
import { Plugin } from '@nocobase/server';
import { resolve } from 'path';
import { getAccessible } from './actions/getAccessible';
export class UiRoutesStoragePlugin extends Plugin {
getName(): string {
return this.getPackageName(__dirname);
}
async install() {
const repository = this.app.db.getRepository('uiRoutes');
const routes = [
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
{
type: 'route',
uiSchema: {
type: 'void',
'x-component': 'Menu',
'x-designer': 'Menu.Designer',
'x-initializer': 'MenuItemInitializers',
'x-component-props': {
mode: 'mix',
theme: 'dark',
// defaultSelectedUid: 'u8',
onSelect: '{{ onSelect }}',
sideMenuRefScopeKey: 'sideMenuRef',
},
properties: {},
},
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: 'NocoBase Admin',
routes: [
// test...
// {
// type: 'route',
// path: '/admin/workflows/:id',
// component: 'WorkflowPage',
// },
// {
// type: 'route',
// path: '/admin/block-templates/:key',
// component: 'BlockTemplateDetails',
// },
// {
// type: 'route',
// path: '/admin/block-templates',
// component: 'BlockTemplatePage',
// },
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'RouteSchemaComponent',
},
],
},
{
type: 'route',
component: 'AuthLayout',
routes: [
{
type: 'route',
path: '/signin',
component: 'SigninPage',
},
{
type: 'route',
path: '/signup',
component: 'SignupPage',
},
],
},
];
for (const values of routes) {
await repository.create({
values,
});
}
}
async load() {
this.app.resourcer.registerActionHandler('uiRoutes:getAccessible', getAccessible);
this.app.db.registerModels({ MagicAttributeModel });
await this.app.db.import({
directory: resolve(__dirname, 'collections'),
});
this.app.acl.use(
skip({
resourceName: 'uiRoutes',
actionName: 'getAccessible',
}),
);
}
}
export default UiRoutesStoragePlugin;

View File

@ -1,6 +1,7 @@
export * from './constants';
export * from './calculators';
export * from './triggers';
export * from './constants';
export * from './instructions';
export { default } from './server';
export * from './triggers';

View File

@ -22,6 +22,7 @@
"@nocobase/plugin-ui-schema-storage": "0.7.0-alpha.83",
"@nocobase/plugin-users": "0.7.0-alpha.83",
"@nocobase/plugin-workflow": "0.7.0-alpha.83",
"@nocobase/plugin-export": "0.7.0-alpha.83",
"@nocobase/server": "0.7.0-alpha.83"
},
"repository": {

106
yarn.lock
View File

@ -2544,6 +2544,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.14.6", "@babel/runtime@^7.17.9", "@babel/runtime@^7.18.3":
version "7.18.3"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2":
version "7.16.7"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
@ -2551,13 +2558,6 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.17.9", "@babel/runtime@^7.18.3":
version "7.18.3"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.10.4", "@babel/template@^7.16.7", "@babel/template@^7.4.0", "@babel/template@^7.4.4":
version "7.16.7"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
@ -5170,6 +5170,11 @@
dependencies:
"@types/express" "*"
"@types/node-xlsx@^0.15.1":
version "0.15.3"
resolved "https://registry.npmmirror.com/@types/node-xlsx/-/node-xlsx-0.15.3.tgz#675e5f91868b6c76c0bd3234fb84e4b55b807b1c"
integrity sha512-6UAa+t9eR3AZzRanZWGNUV8MoXwVpXQHMqCMdK8PNPwTy+G3kYHP+2KkTI+39vXETNb6krRijL+zWzmFrlfLjw==
"@types/node@*":
version "16.11.7"
resolved "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"
@ -5871,6 +5876,19 @@ address@>=0.0.1, address@^1.0.0:
resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==
adler-32@~1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25"
integrity sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==
dependencies:
exit-on-epipe "~1.0.1"
printj "~1.1.0"
adler-32@~1.3.0:
version "1.3.1"
resolved "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2"
integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
agent-base@6, agent-base@^6.0.0, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@ -7187,7 +7205,7 @@ buffer-equal@^1.0.0:
resolved "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74=
buffer-from@1.x, buffer-from@^1.0.0:
buffer-from@1.x, buffer-from@^1.0.0, buffer-from@^1.1.1:
version "1.1.2"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
@ -7466,6 +7484,14 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
cfb@^1.1.4:
version "1.2.2"
resolved "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44"
integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
dependencies:
adler-32 "~1.3.0"
crc-32 "~1.2.0"
chalk@2.4.1:
version "2.4.1"
resolved "https://registry.npmmirror.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
@ -7849,6 +7875,11 @@ code-point-at@^1.0.0:
resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codepage@~1.15.0:
version "1.15.0"
resolved "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab"
integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
@ -8388,6 +8419,11 @@ cosmiconfig@^7, cosmiconfig@^7.0.0:
path-type "^4.0.0"
yaml "^1.10.0"
crc-32@~1.2.0:
version "1.2.2"
resolved "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
create-ecdh@^4.0.0:
version "4.0.4"
resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@ -10202,6 +10238,11 @@ exenv@^1.2.0:
resolved "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
exit-on-epipe@~1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -10676,6 +10717,11 @@ formstream@^1.1.0:
mime "^2.5.2"
pause-stream "~0.0.11"
frac@~1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b"
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@ -15697,6 +15743,15 @@ node-releases@^2.0.3:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476"
integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==
node-xlsx@^0.16.1:
version "0.16.2"
resolved "https://registry.npmmirror.com/node-xlsx/-/node-xlsx-0.16.2.tgz#40f580187eae0e032cac96e958e97cb6ceca09f6"
integrity sha512-ZT3Y4Zg2BFC2UWdp9B/6x3GqrFL0Bf0cXKy9IyhcwlKbcDAf5GuPAPSqrWFQK68NIpfTNA1Kr/NNjpwYxUgHTA==
dependencies:
"@babel/runtime" "^7.14.6"
buffer-from "^1.1.1"
xlsx "^0.17.0"
nodemailer-mock@^1.5.11:
version "1.5.11"
resolved "https://registry.npmjs.org/nodemailer-mock/-/nodemailer-mock-1.5.11.tgz#1bb6b9af44e9007380191d32e33555ea136a819f"
@ -17785,6 +17840,11 @@ pretty-quick@^3.1.0:
mri "^1.1.5"
multimatch "^4.0.0"
printj@~1.1.0:
version "1.1.2"
resolved "https://registry.npmmirror.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
prism-react-renderer@^1.1.1:
version "1.2.1"
resolved "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.2.1.tgz#392460acf63540960e5e3caa699d851264e99b89"
@ -20599,6 +20659,13 @@ sqlstring@^2.3.2:
resolved "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514"
integrity sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg==
ssf@~0.11.2:
version "0.11.2"
resolved "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c"
integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
dependencies:
frac "~1.1.2"
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@ -22779,11 +22846,21 @@ wkx@^0.5.0:
dependencies:
"@types/node" "*"
wmf@~1.0.1:
version "1.0.2"
resolved "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da"
integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
word@~0.3.0:
version "0.3.0"
resolved "https://registry.npmmirror.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961"
integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
wordwrap@0.0.2:
version "0.0.2"
resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
@ -22914,6 +22991,19 @@ xdg-basedir@^3.0.0:
resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
xlsx@^0.17.0:
version "0.17.5"
resolved "https://registry.npmmirror.com/xlsx/-/xlsx-0.17.5.tgz#78b788fcfc0773d126cdcd7ea069cb7527c1ce81"
integrity sha512-lXNU0TuYsvElzvtI6O7WIVb9Zar1XYw7Xb3VAx2wn8N/n0whBYrCnHMxtFyIiUU1Wjf09WzmLALDfBO5PqTb1g==
dependencies:
adler-32 "~1.2.0"
cfb "^1.1.4"
codepage "~1.15.0"
crc-32 "~1.2.0"
ssf "~0.11.2"
wmf "~1.0.1"
word "~0.3.0"
xml-name-validator@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"