feat(workflow): support Http Request Node (#1102)

* feat(workflow): support Http Request Node

* style(workflow): hTTP Request ui title

* style(workflow): request node ui title fix

* feat(workflow): support timeout config,ignoreFail etc

* refactor(workflow): request node Instruction remove unused input.result from templateVars

* fix(workflow): fix locale

* fix(workflow): perfect request implementation
This commit is contained in:
lyf-coder 2022-11-23 18:29:18 +08:00 committed by GitHub
parent 6f4f601ba8
commit 9b4139e28a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 708 additions and 3 deletions

View File

@ -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"

View File

@ -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 (
<Row style={{ width: '100%' }}>
<Col span={10}>
<Operand value={operand} onChange={(v) => setOperand(v)} />
</Col>
<Col span={10}>
<Input value={getVal(operand)} disabled />
</Col>
<Col span={4}>
<CopyToClipboard
text={getVal(operand)}
onCopy={() => {
message.success({
content: t('Copy success!'),
duration: 1,
style: {
marginTop: '20vh',
},
});
}}
>
<Tooltip title={t('Copy variable output template statement')}>
<Button icon={<CopyOutlined />} />
</Tooltip>
</CopyToClipboard>
</Col>
</Row>
);
};
export default function (props) {
const [showTool, setShowTool] = useState(false);
const { t } = useWorkflowTranslation();
const requestVariableTypes = {
$context: VariableTypes.$context,
$jobsMapByNodeId: VariableTypes.$jobsMapByNodeId,
};
return (
<VariableTypesContext.Provider value={requestVariableTypes}>
<Row style={{ width: '100%' }}>
<Col span={24}>
<Tooltip title={t('Show available variable tool')}>
<Button type={'text'} icon={<ToolOutlined />} onClick={() => setShowTool(!showTool)} />
</Tooltip>
</Col>
</Row>
{showTool && <AvailableVariableTool></AvailableVariableTool>}
<Row style={{ width: '100%' }}>
<Col span={24}>
<Input.TextArea {...props} />
</Col>
</Row>
{showTool && (
<Row>
<Col span={24}>
<div className="ant-formily-item-extra">
{props.description}
{' '}
{t('Syntax see')}{' '}
<a target={'_blank'} href={'https://ejs.co'}>
ejs
</a>
{' '}
</div>
</Col>
</Row>
)}
</VariableTypesContext.Provider>
);
}

View File

@ -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'
};

View File

@ -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':'显示可用变量工具'
};

View File

@ -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();

View File

@ -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,
},
};

View File

@ -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' });
});
});
});

View File

@ -43,7 +43,8 @@ export default function<T extends Instruction>(
'query',
'create',
'update',
'destroy'
'destroy',
'request'
].reduce((result, key) => Object.assign(result, {
[key]: requireModule(path.isAbsolute(key) ? key : path.join(__dirname, key))
}), {});

View File

@ -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<AxiosRequestConfig, 'url' | 'method' | 'data' | 'timeout'> & {
headers: Array<Header>;
ignoreFail: boolean;
};
async function renderData(templateStr: string, vars: any): Promise<string> {
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;
}
}

View File

@ -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"