diff --git a/packages/plugins/workflow/package.json b/packages/plugins/workflow/package.json index e82759f3d..f0328133d 100644 --- a/packages/plugins/workflow/package.json +++ b/packages/plugins/workflow/package.json @@ -18,9 +18,12 @@ "classnames": "^2.3.1", "cron-parser": "4.4.0", "json-templates": "^4.2.0", - "react-js-cron": "^1.4.0" + "react-js-cron": "^1.4.0", + "ejs": "^3.1.8", + "react-copy-to-clipboard": "^5.1.0" }, "devDependencies": { + "@types/ejs": "^3.1.1", "@nocobase/test": "0.8.0-alpha.13" }, "gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644" diff --git a/packages/plugins/workflow/src/client/components/EjsTextArea.tsx b/packages/plugins/workflow/src/client/components/EjsTextArea.tsx new file mode 100644 index 000000000..b8a756ae8 --- /dev/null +++ b/packages/plugins/workflow/src/client/components/EjsTextArea.tsx @@ -0,0 +1,121 @@ +import React, { useState } from 'react'; +import { Row, Col } from 'antd'; +import { CopyOutlined, ToolOutlined } from '@ant-design/icons'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; + +import { Operand, VariableTypes, VariableTypesContext } from '../calculators'; +import { Button, Input, message, Tooltip } from 'antd'; +import { useFlowContext } from '../FlowContext'; +import { useCollectionManager, useCompile } from '@nocobase/client'; +import { useWorkflowTranslation } from '../locale'; + +function getVal(operand) { + const { options } = operand; + const { t } = useWorkflowTranslation(); + const compile = useCompile(); + const { getCollectionFields } = useCollectionManager(); + const { nodes } = useFlowContext(); + + switch (operand.type) { + case '$context': + if (options?.path) { + return `<%=ctx.${options.path}%>`; + } + break; + case '$jobsMapByNodeId': + if (options?.nodeId) { + const node = nodes.find((n) => n.id == options.nodeId); + if (node) { + if (node.type === 'calculation') { + return `<%= node.${options.nodeId} // ${t('Calculation result')} %>`; + } + if (options.path && node?.config?.collection) { + const fields = getCollectionFields(node?.config?.collection); + const field = fields.find((f) => f.name == options.path); + if (field) { + return `<%= node[${options.nodeId}].${options.path} // ${compile( + field.uiSchema?.title || field.name, + )} %>`; + } + return `<%= node[${options.nodeId}].${options.path}%>`; + } + } + } + } + return ''; +} + +const AvailableVariableTool = (props) => { + const { t } = useWorkflowTranslation(); + const [operand, setOperand] = useState({ type: '$context', value: '' }); + return ( + + + setOperand(v)} /> + + + + + + { + message.success({ + content: t('Copy success!'), + duration: 1, + style: { + marginTop: '20vh', + }, + }); + }} + > + + } /> + + + + + ); +}; + +export default function (props) { + const [showTool, setShowTool] = useState(false); + const { t } = useWorkflowTranslation(); + const requestVariableTypes = { + $context: VariableTypes.$context, + $jobsMapByNodeId: VariableTypes.$jobsMapByNodeId, + }; + return ( + + + + + } onClick={() => setShowTool(!showTool)} /> + + + + {showTool && } + + + + + + + {showTool && ( + + + + {props.description} + {' '} + {t('Syntax see')}{' '} + + ejs + + {' '} + + + + )} + + ); +} diff --git a/packages/plugins/workflow/src/client/locale/en-US.ts b/packages/plugins/workflow/src/client/locale/en-US.ts index 707aead60..3f637f62b 100644 --- a/packages/plugins/workflow/src/client/locale/en-US.ts +++ b/packages/plugins/workflow/src/client/locale/en-US.ts @@ -85,4 +85,22 @@ export default { "Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.": "Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.", "Trigger in executed workflow cannot be modified": "Trigger in executed workflow cannot be modified", "Node in executed workflow cannot be modified": "Node in executed workflow cannot be modified", + + 'HTTP request': 'HTTP request', + 'URL': 'URL', + 'You can use the above available variables in URL.': 'You can use the above available variables in URL.', + 'Request headers': 'Request headers', + 'Name(e.g. Content-Type)': 'Name(e.g. Content-Type)', + 'Value(e.g. Application/json)': 'Value(e.g. Application/json)', + 'Add request header': 'Add request header', + 'HTTP method': 'HTTP method', + 'Request data': 'Request data', + 'Input request data': 'Input request data', + 'You can use the above available variables in request data.': 'You can use the above available variables in request data.', + 'Copy success!': 'Copy success!', + 'Copy variable output template statement': 'Copy variable output template statement', + 'Default headers is Content-Type: application/json': 'Default headers is Content-Type: application/json', + 'Ignore fail request and continue workflow': 'Ignore fail request and continue workflow', + 'Syntax see': 'Syntax see', + 'Show available variable tool': 'Show available variable tool' }; diff --git a/packages/plugins/workflow/src/client/locale/zh-CN.ts b/packages/plugins/workflow/src/client/locale/zh-CN.ts index d501ee38f..91cbd5591 100644 --- a/packages/plugins/workflow/src/client/locale/zh-CN.ts +++ b/packages/plugins/workflow/src/client/locale/zh-CN.ts @@ -125,4 +125,22 @@ export default { 'Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.': '未被赋值的字段将被设置为默认值,没有默认值的设置为空值。', 'Trigger in executed workflow cannot be modified': '已经执行过工作流的触发器不能被修改', 'Node in executed workflow cannot be modified': '已经执行过工作流中的节点不能被修改', + + 'HTTP request': 'HTTP 请求', + 'URL': '地址', + 'You can use the above available variables in URL.': '您可以在地址中使用使用上面的可用变量。', + 'Request headers': '请求头', + 'Name(e.g. Content-Type)': '名称(例如 Content-Type)', + 'Value(e.g. Application/json)': '值(例如 Application/json)', + 'Add request header': '添加请求头', + 'HTTP method': 'HTTP 方法', + 'Request data': '请求数据', + 'Input request data': '输入请求数据', + 'You can use the above available variables in request data.': '您可以在请求数据中使用上面的可用变量。', + 'Copy success!': '拷贝成功!', + 'Copy variable output template statement': '拷贝变量输出模版语句', + 'Default headers is Content-Type: application/json': '默认请求头是 Content-Type: application/json', + 'Ignore fail request and continue workflow': '忽略失败请求并继续工作流', + 'Syntax see': '语法参考', + 'Show available variable tool':'显示可用变量工具' }; diff --git a/packages/plugins/workflow/src/client/nodes/index.tsx b/packages/plugins/workflow/src/client/nodes/index.tsx index 1f59dc1c2..f18735d92 100644 --- a/packages/plugins/workflow/src/client/nodes/index.tsx +++ b/packages/plugins/workflow/src/client/nodes/index.tsx @@ -25,6 +25,7 @@ import update from './update'; import destroy from './destroy'; import { JobStatusOptions, JobStatusOptionsMap } from '../constants'; import { lang, NAMESPACE } from '../locale'; +import request from "./request"; export interface Instruction { title: string; @@ -51,6 +52,7 @@ instructions.register('query', query); instructions.register('create', create); instructions.register('update', update); instructions.register('destroy', destroy); +instructions.register('request', request); function useUpdateAction() { const form = useForm(); diff --git a/packages/plugins/workflow/src/client/nodes/request.tsx b/packages/plugins/workflow/src/client/nodes/request.tsx new file mode 100644 index 000000000..572f99d49 --- /dev/null +++ b/packages/plugins/workflow/src/client/nodes/request.tsx @@ -0,0 +1,142 @@ +import { NAMESPACE } from '../locale'; +import { ArrayItems } from '@formily/antd'; +import React from 'react'; +import EjsTextArea from '../components/EjsTextArea'; + + + +export default { + title: `{{t("HTTP request", { ns: "${NAMESPACE}" })}}`, + type: 'request', + group: 'extended', + fieldset: { + 'config.url': { + type: 'string', + name: 'config.url', + required: true, + title: `{{t("URL", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-decorator-props': {}, + 'x-component': 'EjsTextArea', + 'x-component-props': { + autoSize: { + minRows: 1, + }, + placeholder: 'https://xxxxxx', + description: `{{t("You can use the above available variables in URL.", { ns: "${NAMESPACE}" })}}`, + }, + }, + 'config.timeout': { + type: 'number', + name: 'config.timeout', + required: true, + title: `{{t("Timeout config", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-decorator-props': {}, + 'x-component': 'InputNumber', + 'x-component-props': { + addonAfter: `{{t("ms", { ns: "${NAMESPACE}" })}}`, + min: 1, + step: 1000, + defaultValue: 5000, + }, + }, + 'config.headers': { + type: 'array', + name: 'config.headers', + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + title: `{{t("Request headers", { ns: "${NAMESPACE}" })}}`, + description: `{{t("Default headers is Content-Type: application/json", { ns: "${NAMESPACE}" })}}`, + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + name: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': { + placeholder: `{{t("Name(e.g. Content-Type)", { ns: "${NAMESPACE}" })}}`, + }, + }, + value: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': { + placeholder: `{{t("Value(e.g. Application/json)", { ns: "${NAMESPACE}" })}}`, + }, + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: `{{t("Add request header", { ns: "${NAMESPACE}" })}}`, + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + 'config.method': { + type: 'string', + name: 'config.method', + required: true, + title: `{{t("HTTP method", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + defaultValue: 'POST', + showSearch: false, + allowClear: false, + }, + enum: [ + { label: 'GET', value: 'GET' }, + { label: 'POST', value: 'POST' }, + { label: 'PUT', value: 'PUT' }, + { label: 'PATCH', value: 'PATCH' }, + { label: 'DELETE', value: 'DELETE' }, + ], + }, + 'config.data': { + type: 'string', + name: 'config.data', + 'x-hidden': false, + title: `{{t("Request data", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-decorator-props': {}, + 'x-component': 'EjsTextArea', + 'x-component-props': { + autoSize: { + minRows: 5, + }, + placeholder: `{{t("Input request data", { ns: "${NAMESPACE}" })}}`, + description: `{{t("You can use the above available variables in request data.", { ns: "${NAMESPACE}" })}}`, + }, + + }, + 'config.ignoreFail': { + type: 'boolean', + name: 'config.ignoreFail', + title: `{{t("Ignore fail request and continue workflow", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + } + }, + view: {}, + scope: {}, + components: { + ArrayItems, + EjsTextArea, + }, +}; diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/request.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/request.test.ts new file mode 100644 index 000000000..325a62948 --- /dev/null +++ b/packages/plugins/workflow/src/server/__tests__/instructions/request.test.ts @@ -0,0 +1,247 @@ +/** + * @jest-environment node + */ + +import { Application } from '@nocobase/server'; +import Database from '@nocobase/database'; +import { getApp, sleep } from '..'; +import { RequestConfig } from '../../instructions/request'; +import axios, { AxiosRequestConfig } from 'axios'; +import { JOB_STATUS } from '../../constants'; + +const testUrl = 'https://nocobase.com/test'; +const url_400 = 'https://nocobase.com/400'; +const timeoutUrl = 'https://nocobase.com/timeout'; +jest.mock('axios', () => { + return { + request: async (config: AxiosRequestConfig) => { + await sleep(1000); + if (config.url === url_400) { + return { + data: config, + status: 400, + }; + } + if (config.url === timeoutUrl) { + throw new Error('timeout'); + } + return { + data: config, + status: 200, + }; + }, + }; +}); + +describe('workflow > instructions > request', () => { + let app: Application; + let db: Database; + let PostRepo; + let WorkflowModel; + let workflow; + + beforeEach(async () => { + app = await getApp(); + + db = app.db; + WorkflowModel = db.getCollection('workflows').model; + PostRepo = db.getCollection('posts').repository; + + workflow = await WorkflowModel.create({ + title: 'test workflow', + enabled: true, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + }); + + afterEach(() => app.stop()); + + describe('request', () => { + it('request', async () => { + await workflow.createNode({ + type: 'request', + config: { + url: 'https://www.baidu.com?name=lily&age=20', + method: 'GET', + } as RequestConfig, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + let [execution] = await workflow.getExecutions(); + let [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.PENDING); + + await sleep(2000); + + [execution] = await workflow.getExecutions(); + [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.RESOLVED); + }); + + it('request - url with variable template', async () => { + await workflow.createNode({ + type: 'request', + config: { + url: 'https://www.xxx.com?title=<%= ctx.data.title // 测试 %>', + method: 'GET', + } as RequestConfig, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + let [execution] = await workflow.getExecutions(); + let [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.PENDING); + + await sleep(1000); + + [execution] = await workflow.getExecutions(); + [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.RESOLVED); + expect(job.result.url).toEqual('https://www.xxx.com?title=t1'); + }); + + it('request - timeout', async () => { + await workflow.createNode({ + type: 'request', + config: { + url: timeoutUrl, + method: 'GET', + timeout: 1000, + } as RequestConfig, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + let [execution] = await workflow.getExecutions(); + let [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.PENDING); + + await sleep(1000); + + [execution] = await workflow.getExecutions(); + [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.REJECTED); + expect(job.result).toMatch('timeout'); + + }); + + it('request - ignoreFail', async () => { + await workflow.createNode({ + type: 'request', + config: { + url: timeoutUrl, + method: 'GET', + timeout: 1000, + ignoreFail: true, + } as RequestConfig, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + let [execution] = await workflow.getExecutions(); + let [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.PENDING); + + await sleep(1000); + + [execution] = await workflow.getExecutions(); + [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.RESOLVED); + expect(job.result).toMatch('timeout'); + }); + + it('response 400', async () => { + await workflow.createNode({ + type: 'request', + config: { + url: url_400, + method: 'GET', + timeout: 1000, + ignoreFail: false, + } as RequestConfig, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + let [execution] = await workflow.getExecutions(); + let [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.PENDING); + + await sleep(1000); + + [execution] = await workflow.getExecutions(); + [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.REJECTED); + expect(job.result.status).toBe(400); + }); + + it('response 400 ignoreFail', async () => { + await workflow.createNode({ + type: 'request', + config: { + url: url_400, + method: 'GET', + timeout: 1000, + ignoreFail: true, + } as RequestConfig, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + let [execution] = await workflow.getExecutions(); + let [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.PENDING); + + await sleep(1000); + + [execution] = await workflow.getExecutions(); + [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.RESOLVED); + expect(job.result.status).toBe(400); + }); + + it('request with data', async () => { + const n1 = await workflow.createNode({ + type: 'request', + config: { + url: testUrl, + method: 'POST', + data: '{"title": "<%=ctx.data.title%>"}', + } as RequestConfig, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + let [execution] = await workflow.getExecutions(); + let [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.PENDING); + + await sleep(1000); + + [execution] = await workflow.getExecutions(); + [job] = await execution.getJobs(); + expect(job.status).toEqual(JOB_STATUS.RESOLVED); + expect(JSON.parse(job.result.data)).toEqual({ title: 't1' }); + }); + }); +}); diff --git a/packages/plugins/workflow/src/server/instructions/index.ts b/packages/plugins/workflow/src/server/instructions/index.ts index 8e538f561..3c81c62a8 100644 --- a/packages/plugins/workflow/src/server/instructions/index.ts +++ b/packages/plugins/workflow/src/server/instructions/index.ts @@ -43,7 +43,8 @@ export default function( 'query', 'create', 'update', - 'destroy' + 'destroy', + 'request' ].reduce((result, key) => Object.assign(result, { [key]: requireModule(path.isAbsolute(key) ? key : path.join(__dirname, key)) }), {}); diff --git a/packages/plugins/workflow/src/server/instructions/request.ts b/packages/plugins/workflow/src/server/instructions/request.ts new file mode 100644 index 000000000..54b4448d6 --- /dev/null +++ b/packages/plugins/workflow/src/server/instructions/request.ts @@ -0,0 +1,95 @@ +import { JOB_STATUS } from '../constants'; +import { render } from 'ejs'; +import axios, { AxiosRequestConfig } from 'axios'; +import _ from 'lodash'; +import { Instruction } from './index'; + +export interface Header { + name: string; + value: any; +} + +export type RequestConfig = Pick & { + headers: Array; + ignoreFail: boolean; +}; + +async function renderData(templateStr: string, vars: any): Promise { + if (_.isEmpty(templateStr)) { + return ''; + } + return render(templateStr, vars, { async: true }); +} + +async function triggerResume(plugin, job, status, result) { + job.set('status', status); + job.set('result', result); + return plugin.resume(job); +} + +export default class implements Instruction { + async run(node, input, processor) { + const templateVars = { + node: processor.jobsMapByNodeId, + ctx: processor.execution.context, + }; + const requestConfig = node.config as RequestConfig; + + // default headers + const headers = { + 'Content-Type': 'application/json', + }; + const { headers: headerArr = [], ignoreFail = false, method = 'POST', timeout = 5000 } = requestConfig; + headerArr.forEach((header) => (headers[header.name] = header.value)); + + let url, data; + try { + url = await renderData(requestConfig.url, templateVars); + } catch (e) { + console.warn(e); + throw new Error(`ejs can't render url, please check url format!${(e as Error)?.message}`); + } + try { + data = await renderData(requestConfig.data, templateVars); + } catch (e) { + console.warn(e); + throw new Error(`ejs can't render request data, please check request data format!${(e as Error)?.message}`); + } + + const job = await processor.saveJob({ + status: JOB_STATUS.PENDING, + nodeId: node.id, + }); + + const plugin = processor.options.plugin; + axios + .request({ + method, + timeout, + headers, + url, + data, + }) + .then((resp) => { + if (resp.status >= 200 && resp.status < 300) { + triggerResume(plugin, job, JOB_STATUS.RESOLVED, resp.data); + } else { + triggerResume(plugin, job, JOB_STATUS.REJECTED, resp); + } + }) + .catch((e) => { + console.warn(e); + triggerResume(plugin, job, JOB_STATUS.REJECTED, (e as Error)?.message); + }); + + return job; + } + + async resume(node, job, processor) { + const { ignoreFail } = node.config as RequestConfig; + if (ignoreFail) { + job.set('status', JOB_STATUS.RESOLVED); + } + return job; + } +} diff --git a/yarn.lock b/yarn.lock index 8360b8ab7..36944869a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5212,6 +5212,11 @@ resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-2.0.1.tgz#ffb6620d290624f3726aa362c0c8a4b44c8d7200" integrity sha512-TF8aoF5cHcLO7W7403blM7L1T+6NF3XMyN3fxyUolq2uOcFeicG/khQg/dGxiCJWoAcmYulYN7LYSRKO54IXaA== +"@types/ejs@^3.1.1": + version "3.1.1" + resolved "https://registry.npmmirror.com/@types/ejs/-/ejs-3.1.1.tgz#29c539826376a65e7f7d672d51301f37ed718f6d" + integrity sha512-RQul5wEfY7BjWm0sYY86cmUN/pcXWGyVxWX93DFFJvcrxax5zKlieLwA3T77xJGwNcZW0YW6CYG70p1m8xPFmA== + "@types/estree@*": version "0.0.51" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" @@ -7404,6 +7409,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -7902,7 +7914,7 @@ chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -8713,6 +8725,13 @@ copy-to-clipboard@^3.2.0: dependencies: toggle-selection "^1.0.6" +copy-to-clipboard@^3.3.1: + version "3.3.3" + resolved "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + copy-to@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" @@ -9936,6 +9955,13 @@ ee-first@1.1.1, ee-first@~1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= +ejs@^3.1.8: + version "3.1.8" + resolved "https://registry.npmmirror.com/ejs/-/ejs-3.1.8.tgz#758d32910c78047585c7ef1f92f9ee041c1c190b" + integrity sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.3.886: version "1.3.896" resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.896.tgz#4a94efe4870b1687eafd5c378198a49da06e8a1b" @@ -10930,6 +10956,13 @@ file-uri-to-path@2: resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba" integrity sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg== +filelist@^1.0.1: + version "1.0.4" + resolved "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -13290,6 +13323,16 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +jake@^10.8.5: + version "10.8.5" + resolved "https://registry.npmmirror.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" + integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.1" + minimatch "^3.0.4" + javascript-natural-sort@^0.7.1: version "0.7.1" resolved "https://registry.npmmirror.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" @@ -15718,6 +15761,13 @@ minimatch@^3.0.3, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -19233,6 +19283,14 @@ react-contenteditable@^3.3.6: fast-deep-equal "^3.1.3" prop-types "^15.7.1" +react-copy-to-clipboard@^5.1.0: + version "5.1.0" + resolved "https://registry.npmmirror.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c" + integrity sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A== + dependencies: + copy-to-clipboard "^3.3.1" + prop-types "^15.8.1" + react-docgen-typescript-dumi-tmp@^1.22.1-0: version "1.22.1-0" resolved "https://registry.npmjs.org/react-docgen-typescript-dumi-tmp/-/react-docgen-typescript-dumi-tmp-1.22.1-0.tgz#6f452de05c5c114a6e1dd60b34930afaa7ae39a0"