feat: introduce CustomAutocomplete with variables
This commit is contained in:
@@ -20,7 +20,7 @@ export default defineAction({
|
|||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Language to translate the text to.',
|
description: 'Language to translate the text to.',
|
||||||
variables: false,
|
variables: true,
|
||||||
value: '',
|
value: '',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Bulgarian', value: 'BG' },
|
{ label: 'Bulgarian', value: 'BG' },
|
||||||
|
@@ -13,7 +13,7 @@ export default defineAction({
|
|||||||
required: true,
|
required: true,
|
||||||
value: null,
|
value: null,
|
||||||
description: 'Delay for unit, e.g. minutes, hours, days, weeks.',
|
description: 'Delay for unit, e.g. minutes, hours, days, weeks.',
|
||||||
variables: false,
|
variables: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Minutes',
|
label: 'Minutes',
|
||||||
|
@@ -11,7 +11,7 @@ export default defineAction({
|
|||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Pick a channel to send the message to.',
|
description: 'Pick a channel to send the message to.',
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
|
@@ -11,7 +11,7 @@ export default defineAction({
|
|||||||
key: 'repo',
|
key: 'repo',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: false,
|
required: false,
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
|
@@ -21,7 +21,7 @@ export default defineAction({
|
|||||||
required: false,
|
required: false,
|
||||||
description:
|
description:
|
||||||
'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
|
'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
@@ -40,7 +40,7 @@ export default defineAction({
|
|||||||
required: true,
|
required: true,
|
||||||
dependsOn: ['parameters.driveId'],
|
dependsOn: ['parameters.driveId'],
|
||||||
description: 'The spreadsheets in your Google Drive.',
|
description: 'The spreadsheets in your Google Drive.',
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
@@ -63,7 +63,7 @@ export default defineAction({
|
|||||||
required: true,
|
required: true,
|
||||||
dependsOn: ['parameters.spreadsheetId'],
|
dependsOn: ['parameters.spreadsheetId'],
|
||||||
description: 'The worksheets in your selected spreadsheet.',
|
description: 'The worksheets in your selected spreadsheet.',
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
|
@@ -84,7 +84,7 @@ export default defineAction({
|
|||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Header key',
|
description: 'Header key',
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
@@ -132,7 +132,7 @@ export default defineAction({
|
|||||||
|
|
||||||
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
|
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
|
||||||
// eslint-disable-next-line no-empty
|
// eslint-disable-next-line no-empty
|
||||||
} catch {}
|
} catch { }
|
||||||
|
|
||||||
const requestData: AxiosRequestConfig = {
|
const requestData: AxiosRequestConfig = {
|
||||||
url,
|
url,
|
||||||
|
@@ -19,7 +19,7 @@ export default defineAction({
|
|||||||
key: 'model',
|
key: 'model',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
@@ -35,7 +35,7 @@ export default defineAction({
|
|||||||
label: 'Messages',
|
label: 'Messages',
|
||||||
key: 'messages',
|
key: 'messages',
|
||||||
type: 'dynamic' as const,
|
type: 'dynamic' as const,
|
||||||
required: false,
|
required: true,
|
||||||
description: 'Add or remove messages as needed',
|
description: 'Add or remove messages as needed',
|
||||||
value: [{ role: 'system', body: '' }],
|
value: [{ role: 'system', body: '' }],
|
||||||
fields: [
|
fields: [
|
||||||
|
@@ -14,7 +14,7 @@ export default defineAction({
|
|||||||
key: 'model',
|
key: 'model',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
|
@@ -18,14 +18,14 @@ export default defineAction({
|
|||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
value: 'public',
|
value: 'public',
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Table name',
|
label: 'Table name',
|
||||||
key: 'table',
|
key: 'table',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Where clause entries',
|
label: 'Where clause entries',
|
||||||
@@ -38,14 +38,14 @@ export default defineAction({
|
|||||||
key: 'columnName',
|
key: 'columnName',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Operator',
|
label: 'Operator',
|
||||||
key: 'operator',
|
key: 'operator',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
options: whereClauseOperators
|
options: whereClauseOperators
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ export default defineAction({
|
|||||||
key: 'parameter',
|
key: 'parameter',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
|
@@ -16,14 +16,14 @@ export default defineAction({
|
|||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
value: 'public',
|
value: 'public',
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Table name',
|
label: 'Table name',
|
||||||
key: 'table',
|
key: 'table',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Column - value entries',
|
label: 'Column - value entries',
|
||||||
@@ -37,7 +37,7 @@ export default defineAction({
|
|||||||
key: 'columnName',
|
key: 'columnName',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
@@ -52,7 +52,7 @@ export default defineAction({
|
|||||||
label: 'Run-time parameters',
|
label: 'Run-time parameters',
|
||||||
key: 'params',
|
key: 'params',
|
||||||
type: 'dynamic' as const,
|
type: 'dynamic' as const,
|
||||||
required: false,
|
required: true,
|
||||||
description: 'Change run-time configuration parameters with SET command',
|
description: 'Change run-time configuration parameters with SET command',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -60,7 +60,7 @@ export default defineAction({
|
|||||||
key: 'parameter',
|
key: 'parameter',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
|
@@ -27,7 +27,7 @@ export default defineAction({
|
|||||||
key: 'parameter',
|
key: 'parameter',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
|
@@ -19,14 +19,14 @@ export default defineAction({
|
|||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
value: 'public',
|
value: 'public',
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Table name',
|
label: 'Table name',
|
||||||
key: 'table',
|
key: 'table',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Where clause entries',
|
label: 'Where clause entries',
|
||||||
@@ -39,14 +39,14 @@ export default defineAction({
|
|||||||
key: 'columnName',
|
key: 'columnName',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Operator',
|
label: 'Operator',
|
||||||
key: 'operator',
|
key: 'operator',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
options: whereClauseOperators
|
options: whereClauseOperators
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,7 @@ export default defineAction({
|
|||||||
key: 'columnName',
|
key: 'columnName',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
@@ -93,7 +93,7 @@ export default defineAction({
|
|||||||
key: 'parameter',
|
key: 'parameter',
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Value',
|
label: 'Value',
|
||||||
|
@@ -10,6 +10,7 @@ export default defineAction({
|
|||||||
key: 'object',
|
key: 'object',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
|
variables: true,
|
||||||
description: 'Pick which type of object you want to search for.',
|
description: 'Pick which type of object you want to search for.',
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
@@ -28,7 +29,7 @@ export default defineAction({
|
|||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
description: 'Pick which field to search by',
|
description: 'Pick which field to search by',
|
||||||
required: true,
|
required: true,
|
||||||
variables: false,
|
variables: true,
|
||||||
dependsOn: ['parameters.object'],
|
dependsOn: ['parameters.object'],
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
|
@@ -23,7 +23,7 @@ export default defineAction({
|
|||||||
'Sort messages by their match strength or by their date. Default is score.',
|
'Sort messages by their match strength or by their date. Default is score.',
|
||||||
required: true,
|
required: true,
|
||||||
value: 'score',
|
value: 'score',
|
||||||
variables: false,
|
variables: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Match strength',
|
label: 'Match strength',
|
||||||
@@ -43,7 +43,7 @@ export default defineAction({
|
|||||||
'Sort matching messages in ascending or descending order. Default is descending.',
|
'Sort matching messages in ascending or descending order. Default is descending.',
|
||||||
required: true,
|
required: true,
|
||||||
value: 'desc',
|
value: 'desc',
|
||||||
variables: false,
|
variables: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Descending (newest or best match first)',
|
label: 'Descending (newest or best match first)',
|
||||||
|
@@ -12,7 +12,7 @@ export default defineAction({
|
|||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Pick a user to send the message to.',
|
description: 'Pick a user to send the message to.',
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
@@ -40,7 +40,7 @@ export default defineAction({
|
|||||||
value: false,
|
value: false,
|
||||||
description:
|
description:
|
||||||
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
|
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
|
||||||
variables: false,
|
variables: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Yes',
|
label: 'Yes',
|
||||||
|
@@ -12,7 +12,7 @@ export default defineAction({
|
|||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: true,
|
required: true,
|
||||||
description: 'Pick a channel to send the message to.',
|
description: 'Pick a channel to send the message to.',
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
@@ -40,7 +40,7 @@ export default defineAction({
|
|||||||
value: false,
|
value: false,
|
||||||
description:
|
description:
|
||||||
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
|
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
|
||||||
variables: false,
|
variables: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Yes',
|
label: 'Yes',
|
||||||
|
@@ -29,7 +29,7 @@ export default defineAction({
|
|||||||
required: false,
|
required: false,
|
||||||
value: false,
|
value: false,
|
||||||
description: 'Sends the message silently. Users will receive a notification with no sound.',
|
description: 'Sends the message silently. Users will receive a notification with no sound.',
|
||||||
variables: false,
|
variables: true,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Yes',
|
label: 'Yes',
|
||||||
|
@@ -10,7 +10,7 @@ export default defineAction({
|
|||||||
key: 'projectId',
|
key: 'projectId',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: false,
|
required: false,
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
@@ -27,7 +27,7 @@ export default defineAction({
|
|||||||
key: 'sectionId',
|
key: 'sectionId',
|
||||||
type: 'dropdown' as const,
|
type: 'dropdown' as const,
|
||||||
required: false,
|
required: false,
|
||||||
variables: false,
|
variables: true,
|
||||||
dependsOn: ['parameters.projectId'],
|
dependsOn: ['parameters.projectId'],
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
|
@@ -13,7 +13,7 @@ export default defineAction({
|
|||||||
required: true,
|
required: true,
|
||||||
description:
|
description:
|
||||||
'The number to send the SMS from. Include country code. Example: 15551234567',
|
'The number to send the SMS from. Include country code. Example: 15551234567',
|
||||||
variables: false,
|
variables: true,
|
||||||
source: {
|
source: {
|
||||||
type: 'query',
|
type: 'query',
|
||||||
name: 'getDynamicData',
|
name: 'getDynamicData',
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import { IDynamicData, IJSONObject } from '@automatisch/types';
|
import { IDynamicData, IJSONObject } from '@automatisch/types';
|
||||||
import Context from '../../types/express/context';
|
import Context from '../../types/express/context';
|
||||||
import App from '../../models/app';
|
import App from '../../models/app';
|
||||||
|
import ExecutionStep from '../../models/execution-step';
|
||||||
import globalVariable from '../../helpers/global-variable';
|
import globalVariable from '../../helpers/global-variable';
|
||||||
|
import computeParameters from '../../helpers/compute-parameters';
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
stepId: string;
|
stepId: string;
|
||||||
@@ -28,18 +30,29 @@ const getDynamicData = async (
|
|||||||
|
|
||||||
if (!connection || !step.appKey) return null;
|
if (!connection || !step.appKey) return null;
|
||||||
|
|
||||||
|
const flow = step.flow;
|
||||||
const app = await App.findOneByKey(step.appKey);
|
const app = await App.findOneByKey(step.appKey);
|
||||||
const $ = await globalVariable({ connection, app, flow: step.flow, step });
|
const $ = await globalVariable({ connection, app, flow, step });
|
||||||
|
|
||||||
const command = app.dynamicData.find(
|
const command = app.dynamicData.find(
|
||||||
(data: IDynamicData) => data.key === params.key
|
(data: IDynamicData) => data.key === params.key
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// apply run-time parameters that're not persisted yet
|
||||||
for (const parameterKey in params.parameters) {
|
for (const parameterKey in params.parameters) {
|
||||||
const parameterValue = params.parameters[parameterKey];
|
const parameterValue = params.parameters[parameterKey];
|
||||||
$.step.parameters[parameterKey] = parameterValue;
|
$.step.parameters[parameterKey] = parameterValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const priorExecutionSteps = await ExecutionStep.query().where({
|
||||||
|
execution_id: (await flow.$relatedQuery('lastExecution')).id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// compute variables in parameters
|
||||||
|
const computedParameters = computeParameters($.step.parameters, priorExecutionSteps);
|
||||||
|
|
||||||
|
$.step.parameters = computedParameters;
|
||||||
|
|
||||||
const fetchedData = await command.run($);
|
const fetchedData = await command.run($);
|
||||||
|
|
||||||
if (fetchedData.error) {
|
if (fetchedData.error) {
|
||||||
|
@@ -21,6 +21,7 @@ class Flow extends Base {
|
|||||||
published_at: string;
|
published_at: string;
|
||||||
remoteWebhookId: string;
|
remoteWebhookId: string;
|
||||||
executions?: Execution[];
|
executions?: Execution[];
|
||||||
|
lastExecution?: Execution;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
|
||||||
static tableName = 'flows';
|
static tableName = 'flows';
|
||||||
@@ -58,6 +59,17 @@ class Flow extends Base {
|
|||||||
to: 'executions.flow_id',
|
to: 'executions.flow_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
lastExecution: {
|
||||||
|
relation: Base.HasOneRelation,
|
||||||
|
modelClass: Execution,
|
||||||
|
join: {
|
||||||
|
from: 'flows.id',
|
||||||
|
to: 'executions.flow_id',
|
||||||
|
},
|
||||||
|
filter(builder: ExtendedQueryBuilder<Execution>) {
|
||||||
|
builder.orderBy('created_at', 'desc').limit(1).first();
|
||||||
|
},
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
relation: Base.HasOneRelation,
|
relation: Base.HasOneRelation,
|
||||||
modelClass: User,
|
modelClass: User,
|
||||||
@@ -89,10 +101,7 @@ class Flow extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async lastInternalId() {
|
async lastInternalId() {
|
||||||
const lastExecution = await this.$relatedQuery('executions')
|
const lastExecution = await this.$relatedQuery('lastExecution');
|
||||||
.orderBy('created_at', 'desc')
|
|
||||||
.limit(1)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return lastExecution ? (lastExecution as Execution).internalId : null;
|
return lastExecution ? (lastExecution as Execution).internalId : null;
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
"@types/node": "^12.0.0",
|
"@types/node": "^12.0.0",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
|
"@types/react-window": "^1.8.5",
|
||||||
"@types/uuid": "^9.0.0",
|
"@types/uuid": "^9.0.0",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"compare-versions": "^4.1.3",
|
"compare-versions": "^4.1.3",
|
||||||
@@ -35,9 +36,10 @@
|
|||||||
"react-json-tree": "^0.16.2",
|
"react-json-tree": "^0.16.2",
|
||||||
"react-router-dom": "^6.0.2",
|
"react-router-dom": "^6.0.2",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
"slate": "^0.72.8",
|
"react-window": "^1.8.9",
|
||||||
"slate-history": "^0.66.0",
|
"slate": "^0.94.1",
|
||||||
"slate-react": "^0.72.9",
|
"slate-history": "^0.93.0",
|
||||||
|
"slate-react": "^0.94.2",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^1.0.1",
|
"web-vitals": "^1.0.1",
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Controller as RHFController, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface ControllerProps {
|
||||||
|
defaultValue?: string;
|
||||||
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
|
shouldUnregister?: boolean;
|
||||||
|
children: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Controller(
|
||||||
|
props: ControllerProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
const {
|
||||||
|
defaultValue = '',
|
||||||
|
name,
|
||||||
|
required,
|
||||||
|
shouldUnregister,
|
||||||
|
children,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RHFController
|
||||||
|
rules={{ required }}
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
shouldUnregister={shouldUnregister ?? false}
|
||||||
|
render={({
|
||||||
|
field,
|
||||||
|
}) => React.cloneElement(children, { field })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Controller;
|
@@ -0,0 +1,96 @@
|
|||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Popper from '@mui/material/Popper';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import type { IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
import Suggestions from 'components/PowerInput/Suggestions';
|
||||||
|
import TabPanel from 'components/TabPanel';
|
||||||
|
|
||||||
|
import Options from './Options';
|
||||||
|
import { Tabs } from './style';
|
||||||
|
|
||||||
|
interface CustomOptionsProps {
|
||||||
|
open: boolean;
|
||||||
|
anchorEl: any;
|
||||||
|
data: any;
|
||||||
|
options: readonly IFieldDropdownOption[];
|
||||||
|
onSuggestionClick: any;
|
||||||
|
onOptionClick: (event: React.MouseEvent, option: any) => void;
|
||||||
|
onTabChange: (tabIndex: 0 | 1) => void;
|
||||||
|
label?: string;
|
||||||
|
initialTabIndex?: 0 | 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomOptions = (props: CustomOptionsProps) => {
|
||||||
|
const {
|
||||||
|
open,
|
||||||
|
anchorEl,
|
||||||
|
data,
|
||||||
|
options = [],
|
||||||
|
onSuggestionClick,
|
||||||
|
onOptionClick,
|
||||||
|
onTabChange,
|
||||||
|
label,
|
||||||
|
initialTabIndex,
|
||||||
|
} = props;
|
||||||
|
const [activeTabIndex, setActiveTabIndex] = React.useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
React.useEffect(function applyInitialActiveTabIndex() {
|
||||||
|
setActiveTabIndex((currentActiveTabIndex) => {
|
||||||
|
if (currentActiveTabIndex === undefined) {
|
||||||
|
return initialTabIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentActiveTabIndex;
|
||||||
|
});
|
||||||
|
}, [initialTabIndex]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
style={{ width: anchorEl?.clientWidth, zIndex: 1 }}
|
||||||
|
modifiers={[
|
||||||
|
{
|
||||||
|
name: 'flip',
|
||||||
|
enabled: false,
|
||||||
|
options: {
|
||||||
|
altBoundary: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Paper elevation={5} sx={{ width: '100%' }}>
|
||||||
|
<Tabs
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
value={activeTabIndex ?? 0}
|
||||||
|
onChange={(event, tabIndex) => {
|
||||||
|
onTabChange(tabIndex);
|
||||||
|
setActiveTabIndex(tabIndex);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab label={label} />
|
||||||
|
<Tab label="Custom" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<TabPanel value={activeTabIndex ?? 0} index={0}>
|
||||||
|
<Options
|
||||||
|
data={options}
|
||||||
|
onOptionClick={onOptionClick}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={activeTabIndex ?? 0} index={1}>
|
||||||
|
<Suggestions
|
||||||
|
data={data}
|
||||||
|
onSuggestionClick={onSuggestionClick}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</Paper>
|
||||||
|
</Popper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default CustomOptions;
|
@@ -0,0 +1,126 @@
|
|||||||
|
import type { IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FixedSizeList, ListChildComponentProps } from 'react-window';
|
||||||
|
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import SearchInput from 'components/SearchInput';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import { SearchInputWrapper } from './style';
|
||||||
|
|
||||||
|
interface OptionsProps {
|
||||||
|
data: readonly IFieldDropdownOption[];
|
||||||
|
onOptionClick: (event: React.MouseEvent, option: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SHORT_LIST_LENGTH = 4;
|
||||||
|
const LIST_ITEM_HEIGHT = 64;
|
||||||
|
|
||||||
|
const computeListHeight = (currentLength: number) => {
|
||||||
|
const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength);
|
||||||
|
return LIST_ITEM_HEIGHT * numberOfRenderedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderItemFactory = ({ onOptionClick }: Pick<OptionsProps, 'onOptionClick'>) => (props: ListChildComponentProps) => {
|
||||||
|
const { index, style, data } = props;
|
||||||
|
|
||||||
|
const suboption = data[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
sx={{ pl: 4 }}
|
||||||
|
divider
|
||||||
|
onClick={(event) => onOptionClick(event, suboption)}
|
||||||
|
data-test="power-input-suggestion-item"
|
||||||
|
key={index}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={suboption.label}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
variant: 'subtitle1',
|
||||||
|
title: 'Property name',
|
||||||
|
sx: { fontWeight: 700 },
|
||||||
|
}}
|
||||||
|
secondary={suboption.value}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
variant: 'subtitle2',
|
||||||
|
title: 'Sample value',
|
||||||
|
noWrap: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Options = (props: OptionsProps) => {
|
||||||
|
const formatMessage = useFormatMessage();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
onOptionClick
|
||||||
|
} = props;
|
||||||
|
const [filteredData, setFilteredData] = React.useState<readonly IFieldDropdownOption[]>(
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(function syncOptions() {
|
||||||
|
setFilteredData((filteredData) => {
|
||||||
|
if (filteredData.length === 0 && filteredData.length !== data.length) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredData;
|
||||||
|
})
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const renderItem = React.useMemo(() => renderItemFactory({
|
||||||
|
onOptionClick
|
||||||
|
}), [onOptionClick]);
|
||||||
|
|
||||||
|
const onSearchChange = React.useMemo(
|
||||||
|
() =>
|
||||||
|
throttle((event: React.ChangeEvent) => {
|
||||||
|
const search = (event.target as HTMLInputElement).value.toLowerCase();
|
||||||
|
|
||||||
|
if (!search) {
|
||||||
|
setFilteredData(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFilteredData = data.filter(option => `${option.label}\n${option.value}`.toLowerCase().includes(search.toLowerCase()));
|
||||||
|
|
||||||
|
setFilteredData(newFilteredData);
|
||||||
|
}, 400),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SearchInputWrapper>
|
||||||
|
<SearchInput onChange={onSearchChange} />
|
||||||
|
</SearchInputWrapper>
|
||||||
|
|
||||||
|
<FixedSizeList
|
||||||
|
height={computeListHeight(filteredData.length)}
|
||||||
|
width="100%"
|
||||||
|
itemSize={LIST_ITEM_HEIGHT}
|
||||||
|
itemCount={filteredData.length}
|
||||||
|
overscanCount={2}
|
||||||
|
itemData={filteredData}
|
||||||
|
>
|
||||||
|
{renderItem}
|
||||||
|
</FixedSizeList>
|
||||||
|
|
||||||
|
{filteredData.length === 0 && (
|
||||||
|
<Typography sx={{ p: (theme) => theme.spacing(0, 0, 2, 2) }}>
|
||||||
|
{formatMessage('customAutocomplete.noOptions')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default Options;
|
@@ -0,0 +1,280 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useController, useFormContext } from 'react-hook-form';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import { AutocompleteProps } from '@mui/material/Autocomplete';
|
||||||
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
|
import type { IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
import { FakeDropdownButton } from './style';
|
||||||
|
|
||||||
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import { createEditor } from 'slate';
|
||||||
|
import { Editable, ReactEditor,} from 'slate-react';
|
||||||
|
|
||||||
|
import Slate from 'components/Slate';
|
||||||
|
import Element from 'components/Slate/Element';
|
||||||
|
|
||||||
|
import {
|
||||||
|
serialize,
|
||||||
|
deserialize,
|
||||||
|
insertVariable,
|
||||||
|
customizeEditor,
|
||||||
|
resetEditor,
|
||||||
|
overrideEditorValue,
|
||||||
|
focusEditor,
|
||||||
|
} from 'components/Slate/utils';
|
||||||
|
import { FakeInput, InputLabelWrapper, ChildrenWrapper, } from 'components/PowerInput/style';
|
||||||
|
import { VariableElement } from 'components/Slate/types';
|
||||||
|
import CustomOptions from './CustomOptions';
|
||||||
|
import { processStepWithExecutions } from 'components/PowerInput/data';
|
||||||
|
import { StepExecutionsContext } from 'contexts/StepExecutions';
|
||||||
|
|
||||||
|
interface ControlledCustomAutocompleteProps
|
||||||
|
extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
||||||
|
showOptionValue?: boolean;
|
||||||
|
dependsOn?: string[];
|
||||||
|
|
||||||
|
defaultValue?: string;
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
type?: string;
|
||||||
|
required?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
description?: string;
|
||||||
|
docUrl?: string;
|
||||||
|
clickToCopy?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
shouldUnregister?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlledCustomAutocomplete(
|
||||||
|
props: ControlledCustomAutocompleteProps
|
||||||
|
): React.ReactElement {
|
||||||
|
const {
|
||||||
|
defaultValue = '',
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
options = [],
|
||||||
|
dependsOn = [],
|
||||||
|
description,
|
||||||
|
loading,
|
||||||
|
disabled,
|
||||||
|
shouldUnregister,
|
||||||
|
} = props;
|
||||||
|
const { control, watch } = useFormContext();
|
||||||
|
const { field, fieldState } = useController({
|
||||||
|
control,
|
||||||
|
name,
|
||||||
|
defaultValue,
|
||||||
|
rules: { required },
|
||||||
|
shouldUnregister,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange: controllerOnChange,
|
||||||
|
onBlur: controllerOnBlur,
|
||||||
|
} = field;
|
||||||
|
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
|
||||||
|
const [isInitialValueSet, setInitialValue] = React.useState(false);
|
||||||
|
const [isSingleChoice, setSingleChoice] = React.useState<boolean | undefined>(undefined);
|
||||||
|
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||||
|
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
const renderElement = React.useCallback(
|
||||||
|
(props) => <Element {...props} disabled={disabled} />,
|
||||||
|
[disabled]
|
||||||
|
);
|
||||||
|
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
||||||
|
const [showVariableSuggestions, setShowVariableSuggestions] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
|
let dependsOnValues: unknown[] = [];
|
||||||
|
if (dependsOn?.length) {
|
||||||
|
dependsOnValues = watch(dependsOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const ref = ReactEditor.toDOMNode(editor, editor);
|
||||||
|
|
||||||
|
resizeObserver.observe(ref);
|
||||||
|
|
||||||
|
return () => resizeObserver.unobserve(ref);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const promoteValue = () => {
|
||||||
|
const serializedValue = serialize(editor.children);
|
||||||
|
controllerOnChange(serializedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = React.useMemo(function syncCustomOptionsPosition() {
|
||||||
|
return new ResizeObserver(() => {
|
||||||
|
forceUpdate();
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const hasDependencies = dependsOnValues.length;
|
||||||
|
|
||||||
|
if (hasDependencies) {
|
||||||
|
// Reset the field when a dependent has been updated
|
||||||
|
resetEditor(editor);
|
||||||
|
}
|
||||||
|
}, dependsOnValues);
|
||||||
|
|
||||||
|
React.useEffect(function updateInitialValue() {
|
||||||
|
const hasOptions = options.length;
|
||||||
|
const isOptionsLoaded = loading === false;
|
||||||
|
if (!isInitialValueSet && hasOptions && isOptionsLoaded) {
|
||||||
|
setInitialValue(true);
|
||||||
|
|
||||||
|
const option: IFieldDropdownOption | undefined = options.find((option) => option.value === value);
|
||||||
|
|
||||||
|
if (option) {
|
||||||
|
overrideEditorValue(editor, { option, focus: false });
|
||||||
|
setSingleChoice(true);
|
||||||
|
} else if (value) {
|
||||||
|
setSingleChoice(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isInitialValueSet, options, loading]);
|
||||||
|
|
||||||
|
const hideSuggestionsOnShift = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.code === 'Tab') {
|
||||||
|
setShowVariableSuggestions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
hideSuggestionsOnShift(event);
|
||||||
|
if (event.code === 'Tab') {
|
||||||
|
promoteValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSingleChoice && event.code !== 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepsWithVariables = React.useMemo(() => {
|
||||||
|
return processStepWithExecutions(priorStepsWithExecutions);
|
||||||
|
}, [priorStepsWithExecutions]);
|
||||||
|
|
||||||
|
const handleVariableSuggestionClick = React.useCallback(
|
||||||
|
(variable: Pick<VariableElement, 'name' | 'value'>) => {
|
||||||
|
insertVariable(editor, variable, stepsWithVariables);
|
||||||
|
},
|
||||||
|
[stepsWithVariables]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionClick = React.useCallback(
|
||||||
|
(event: React.MouseEvent, option: IFieldDropdownOption) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
overrideEditorValue(editor, { option, focus: false });
|
||||||
|
|
||||||
|
setShowVariableSuggestions(false);
|
||||||
|
|
||||||
|
promoteValue();
|
||||||
|
},
|
||||||
|
[stepsWithVariables]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reset = (tabIndex: 0 | 1) => {
|
||||||
|
const isOptions = tabIndex === 0;
|
||||||
|
|
||||||
|
setSingleChoice(isOptions);
|
||||||
|
|
||||||
|
resetEditor(editor, { focus: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slate
|
||||||
|
editor={editor}
|
||||||
|
value={deserialize(value, options, stepsWithVariables)}
|
||||||
|
>
|
||||||
|
<ClickAwayListener
|
||||||
|
mouseEvent="onMouseDown"
|
||||||
|
onClickAway={() => {
|
||||||
|
promoteValue();
|
||||||
|
|
||||||
|
setShowVariableSuggestions(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ref-able single child for ClickAwayListener */}
|
||||||
|
<ChildrenWrapper style={{ width: '100%' }} data-test="power-input">
|
||||||
|
<FakeInput
|
||||||
|
disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => {
|
||||||
|
focusEditor(editor);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputLabelWrapper>
|
||||||
|
<InputLabel
|
||||||
|
shrink={true}
|
||||||
|
disabled={disabled}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ bgcolor: 'white', display: 'inline-block', px: 0.75 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</InputLabel>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
|
||||||
|
<Editable
|
||||||
|
readOnly={disabled}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
renderElement={renderElement}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onFocus={() => {
|
||||||
|
setShowVariableSuggestions(true);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
controllerOnBlur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FakeDropdownButton
|
||||||
|
disabled={disabled}
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<ArrowDropDownIcon />
|
||||||
|
</FakeDropdownButton>
|
||||||
|
</FakeInput>
|
||||||
|
{/* ghost placer for the variables popover */}
|
||||||
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 16,
|
||||||
|
left: 16
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CustomOptions
|
||||||
|
label={label}
|
||||||
|
open={showVariableSuggestions}
|
||||||
|
initialTabIndex={isSingleChoice === undefined ? undefined : (isSingleChoice ? 0 : 1)}
|
||||||
|
anchorEl={editorRef.current}
|
||||||
|
data={stepsWithVariables}
|
||||||
|
options={options}
|
||||||
|
onSuggestionClick={handleVariableSuggestionClick}
|
||||||
|
onOptionClick={handleOptionClick}
|
||||||
|
onTabChange={reset}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormHelperText
|
||||||
|
variant="outlined"
|
||||||
|
error={Boolean(fieldState.isTouched && fieldState.error)}
|
||||||
|
>
|
||||||
|
{fieldState.isTouched
|
||||||
|
? fieldState.error?.message || description
|
||||||
|
: description}
|
||||||
|
</FormHelperText>
|
||||||
|
</ChildrenWrapper>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Slate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ControlledCustomAutocomplete;
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import MuiIconButton from '@mui/material/IconButton';
|
||||||
|
import MuiTabs from '@mui/material/Tabs';
|
||||||
|
|
||||||
|
export const FakeDropdownButton = styled(MuiIconButton)`
|
||||||
|
position: absolute;
|
||||||
|
right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Tabs = styled(MuiTabs)`
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SearchInputWrapper = styled('div')`
|
||||||
|
padding: ${({ theme }) => theme.spacing(0, 2, 2, 2)};
|
||||||
|
`;
|
@@ -41,6 +41,7 @@ import {
|
|||||||
Header,
|
Header,
|
||||||
Wrapper,
|
Wrapper,
|
||||||
} from './style';
|
} from './style';
|
||||||
|
import isEmpty from 'helpers/isEmpty';
|
||||||
|
|
||||||
type FlowStepProps = {
|
type FlowStepProps = {
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
@@ -75,7 +76,13 @@ function generateValidationSchema(substeps: ISubstep[]) {
|
|||||||
if (required) {
|
if (required) {
|
||||||
substepArgumentValidations[key] = substepArgumentValidations[
|
substepArgumentValidations[key] = substepArgumentValidations[
|
||||||
key
|
key
|
||||||
].required(`${key} is required.`);
|
]
|
||||||
|
.required(`${key} is required.`)
|
||||||
|
.test(
|
||||||
|
'empty-check',
|
||||||
|
`${key} must be not empty`,
|
||||||
|
(value: any) => !isEmpty(value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the field depends on another field, add the dependsOn required validation
|
// if the field depends on another field, add the dependsOn required validation
|
||||||
|
@@ -4,7 +4,7 @@ import Collapse from '@mui/material/Collapse';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import type { IField, IStep, ISubstep } from '@automatisch/types';
|
import type { IStep, ISubstep } from '@automatisch/types';
|
||||||
|
|
||||||
import { EditorContext } from 'contexts/Editor';
|
import { EditorContext } from 'contexts/Editor';
|
||||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||||
@@ -21,25 +21,6 @@ type FlowSubstepProps = {
|
|||||||
step: IStep;
|
step: IStep;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateSubstep = (substep: ISubstep, step: IStep) => {
|
|
||||||
if (!substep) return true;
|
|
||||||
|
|
||||||
const args: IField[] = substep.arguments || [];
|
|
||||||
|
|
||||||
return args.every((arg) => {
|
|
||||||
if (arg.required === false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const argValue = step.parameters?.[arg.key];
|
|
||||||
|
|
||||||
// `false` is an exceptional valid value
|
|
||||||
if (argValue === false) return true;
|
|
||||||
|
|
||||||
return argValue !== undefined && argValue !== null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
substep,
|
substep,
|
||||||
@@ -54,19 +35,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
|
|
||||||
const editorContext = React.useContext(EditorContext);
|
const editorContext = React.useContext(EditorContext);
|
||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
const [validationStatus, setValidationStatus] = React.useState<
|
const validationStatus = formContext.formState.isValid;
|
||||||
boolean | null
|
|
||||||
>(validateSubstep(substep, formContext.getValues() as IStep));
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
function validate(step: unknown) {
|
|
||||||
const validationResult = validateSubstep(substep, step as IStep);
|
|
||||||
setValidationStatus(validationResult);
|
|
||||||
}
|
|
||||||
const subscription = formContext.watch(validate);
|
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
}, [substep, formContext.watch]);
|
|
||||||
|
|
||||||
const onToggle = expanded ? onCollapse : onExpand;
|
const onToggle = expanded ? onCollapse : onExpand;
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ import useDynamicData from 'hooks/useDynamicData';
|
|||||||
import PowerInput from 'components/PowerInput';
|
import PowerInput from 'components/PowerInput';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
import ControlledAutocomplete from 'components/ControlledAutocomplete';
|
||||||
|
import ControlledCustomAutocomplete from 'components/ControlledCustomAutocomplete';
|
||||||
import DynamicField from 'components/DynamicField';
|
import DynamicField from 'components/DynamicField';
|
||||||
|
|
||||||
type InputCreatorProps = {
|
type InputCreatorProps = {
|
||||||
@@ -81,22 +82,44 @@ export default function InputCreator(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ControlledAutocomplete
|
{!schema.variables && (
|
||||||
key={computedName}
|
<ControlledAutocomplete
|
||||||
name={computedName}
|
key={computedName}
|
||||||
dependsOn={schema.dependsOn}
|
name={computedName}
|
||||||
fullWidth
|
dependsOn={schema.dependsOn}
|
||||||
disablePortal
|
fullWidth
|
||||||
disableClearable={required}
|
disablePortal
|
||||||
options={preparedOptions}
|
disableClearable={required}
|
||||||
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
options={preparedOptions}
|
||||||
defaultValue={value as string}
|
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
||||||
description={description}
|
defaultValue={value as string}
|
||||||
loading={loading}
|
description={description}
|
||||||
disabled={disabled}
|
loading={loading}
|
||||||
showOptionValue={showOptionValue}
|
disabled={disabled}
|
||||||
shouldUnregister={shouldUnregister}
|
showOptionValue={showOptionValue}
|
||||||
/>
|
shouldUnregister={shouldUnregister}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{schema.variables && (
|
||||||
|
<ControlledCustomAutocomplete
|
||||||
|
key={computedName}
|
||||||
|
name={computedName}
|
||||||
|
dependsOn={schema.dependsOn}
|
||||||
|
label={label}
|
||||||
|
fullWidth
|
||||||
|
disablePortal
|
||||||
|
disableClearable={required}
|
||||||
|
options={preparedOptions}
|
||||||
|
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
||||||
|
defaultValue={value as string}
|
||||||
|
description={description}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
showOptionValue={showOptionValue}
|
||||||
|
shouldUnregister={shouldUnregister}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
61
packages/web/src/components/PowerInput/Popper.tsx
Normal file
61
packages/web/src/components/PowerInput/Popper.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import MuiPopper from '@mui/material/Popper';
|
||||||
|
import Tab from '@mui/material/Tab';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import Suggestions from 'components/PowerInput/Suggestions';
|
||||||
|
import TabPanel from 'components/TabPanel';
|
||||||
|
|
||||||
|
import { Tabs } from './style';
|
||||||
|
|
||||||
|
interface PopperProps {
|
||||||
|
open: boolean;
|
||||||
|
anchorEl: any;
|
||||||
|
data: any;
|
||||||
|
onSuggestionClick: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Popper = (props: PopperProps) => {
|
||||||
|
const {
|
||||||
|
open,
|
||||||
|
anchorEl,
|
||||||
|
data,
|
||||||
|
onSuggestionClick,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiPopper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
style={{ width: anchorEl?.clientWidth, zIndex: 1 }}
|
||||||
|
modifiers={[
|
||||||
|
{
|
||||||
|
name: 'flip',
|
||||||
|
enabled: false,
|
||||||
|
options: {
|
||||||
|
altBoundary: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Paper elevation={5} sx={{ width: '100%' }}>
|
||||||
|
<Tabs
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
value={0}
|
||||||
|
>
|
||||||
|
<Tab label="Insert data..." />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<TabPanel value={0} index={0}>
|
||||||
|
<Suggestions
|
||||||
|
data={data}
|
||||||
|
onSuggestionClick={onSuggestionClick}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</Paper>
|
||||||
|
</MuiPopper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default Popper;
|
@@ -1,35 +1,99 @@
|
|||||||
import * as React from 'react';
|
import type { IStep } from '@automatisch/types';
|
||||||
|
|
||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import List from '@mui/material/List';
|
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
|
||||||
import MuiListItemText from '@mui/material/ListItemText';
|
|
||||||
import Paper from '@mui/material/Paper';
|
|
||||||
import Collapse from '@mui/material/Collapse';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import ExpandLess from '@mui/icons-material/ExpandLess';
|
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||||
import ExpandMore from '@mui/icons-material/ExpandMore';
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||||
import type { IStep } from '@automatisch/types';
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FixedSizeList, ListChildComponentProps } from 'react-window';
|
||||||
|
|
||||||
const ListItemText = styled(MuiListItemText)``;
|
import SearchInput from 'components/SearchInput';
|
||||||
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
type SuggestionsProps = {
|
type SuggestionsProps = {
|
||||||
data: any[];
|
data: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
output: Record<string, unknown>[]
|
||||||
|
}[];
|
||||||
onSuggestionClick: (variable: any) => void;
|
onSuggestionClick: (variable: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SHORT_LIST_LENGTH = 4;
|
const SHORT_LIST_LENGTH = 4;
|
||||||
const LIST_HEIGHT = 256;
|
const LIST_ITEM_HEIGHT = 64;
|
||||||
|
|
||||||
|
const computeListHeight = (currentLength: number) => {
|
||||||
|
const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength);
|
||||||
|
return LIST_ITEM_HEIGHT * numberOfRenderedItems;
|
||||||
|
}
|
||||||
|
|
||||||
const getPartialArray = (array: any[], length = array.length) => {
|
const getPartialArray = (array: any[], length = array.length) => {
|
||||||
return array.slice(0, length);
|
return array.slice(0, length);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderItemFactory = ({ onSuggestionClick }: Pick<SuggestionsProps, 'onSuggestionClick'>) => (props: ListChildComponentProps) => {
|
||||||
|
const { index, style, data } = props;
|
||||||
|
|
||||||
|
const suboption = data[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
sx={{ pl: 4 }}
|
||||||
|
divider
|
||||||
|
onClick={() => onSuggestionClick(suboption)}
|
||||||
|
data-test="power-input-suggestion-item"
|
||||||
|
key={index}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={suboption.label}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
variant: 'subtitle1',
|
||||||
|
title: 'Property name',
|
||||||
|
sx: { fontWeight: 700 },
|
||||||
|
}}
|
||||||
|
secondary={suboption.sampleValue || ''}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
variant: 'subtitle2',
|
||||||
|
title: 'Sample value',
|
||||||
|
noWrap: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const Suggestions = (props: SuggestionsProps) => {
|
const Suggestions = (props: SuggestionsProps) => {
|
||||||
const { data, onSuggestionClick = () => null } = props;
|
const formatMessage = useFormatMessage();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
onSuggestionClick = () => null
|
||||||
|
} = props;
|
||||||
const [current, setCurrent] = React.useState<number | null>(0);
|
const [current, setCurrent] = React.useState<number | null>(0);
|
||||||
const [listLength, setListLength] = React.useState<number>(SHORT_LIST_LENGTH);
|
const [listLength, setListLength] = React.useState<number>(SHORT_LIST_LENGTH);
|
||||||
|
const [filteredData, setFilteredData] = React.useState<any[]>(
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(function syncOptions() {
|
||||||
|
setFilteredData((filteredData) => {
|
||||||
|
if (filteredData.length === 0 && filteredData.length !== data.length) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredData;
|
||||||
|
})
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const renderItem = React.useMemo(() => renderItemFactory({
|
||||||
|
onSuggestionClick
|
||||||
|
}), [onSuggestionClick]);
|
||||||
|
|
||||||
const expandList = () => {
|
const expandList = () => {
|
||||||
setListLength(Infinity);
|
setListLength(Infinity);
|
||||||
@@ -43,79 +107,95 @@ const Suggestions = (props: SuggestionsProps) => {
|
|||||||
setListLength(SHORT_LIST_LENGTH);
|
setListLength(SHORT_LIST_LENGTH);
|
||||||
}, [current]);
|
}, [current]);
|
||||||
|
|
||||||
return (
|
const onSearchChange = React.useMemo(
|
||||||
<Paper elevation={5} sx={{ width: '100%' }}>
|
() =>
|
||||||
<Typography variant="subtitle2" sx={{ p: 2 }}>
|
throttle((event: React.ChangeEvent) => {
|
||||||
Variables
|
const search = (event.target as HTMLInputElement).value.toLowerCase();
|
||||||
</Typography>
|
|
||||||
<List disablePadding>
|
if (!search) {
|
||||||
{data.map((option: IStep, index: number) => (
|
setFilteredData(data);
|
||||||
<>
|
return;
|
||||||
<ListItemButton
|
}
|
||||||
divider
|
|
||||||
onClick={() =>
|
const newFilteredData = data
|
||||||
setCurrent((currentIndex) =>
|
.map((stepWithOutput) => {
|
||||||
currentIndex === index ? null : index
|
return {
|
||||||
|
id: stepWithOutput.id,
|
||||||
|
name: stepWithOutput.name,
|
||||||
|
output: stepWithOutput.output
|
||||||
|
.filter(option => `${option.label}\n${option.sampleValue}`
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sx={{ py: 0.5 }}
|
})
|
||||||
>
|
.filter((stepWithOutput) => stepWithOutput.output.length);
|
||||||
<ListItemText primary={option.name} />
|
|
||||||
|
|
||||||
{!!option.output?.length &&
|
setFilteredData(newFilteredData);
|
||||||
(current === index ? <ExpandLess /> : <ExpandMore />)}
|
}, 400),
|
||||||
</ListItemButton>
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
<Collapse in={current === index} timeout="auto" unmountOnExit>
|
return (
|
||||||
<List
|
<Paper elevation={0} sx={{ width: '100%' }}>
|
||||||
component="div"
|
<Box px={2} pb={2}>
|
||||||
disablePadding
|
<SearchInput onChange={onSearchChange} />
|
||||||
sx={{ maxHeight: LIST_HEIGHT, overflowY: 'auto' }}
|
</Box>
|
||||||
data-test="power-input-suggestion-group"
|
|
||||||
>
|
{filteredData.length > 0 && (
|
||||||
{getPartialArray((option.output as any) || [], listLength).map(
|
<List disablePadding>
|
||||||
(suboption: any, index: number) => (
|
{filteredData.map((option: IStep, index: number) => (
|
||||||
<ListItemButton
|
<React.Fragment key={`${index}-${option.name}`}>
|
||||||
sx={{ pl: 4 }}
|
<ListItemButton
|
||||||
divider
|
divider
|
||||||
onClick={() => onSuggestionClick(suboption)}
|
onClick={() =>
|
||||||
data-test="power-input-suggestion-item"
|
setCurrent((currentIndex) =>
|
||||||
key={`suggestion-${suboption.name}`}
|
currentIndex === index ? null : index
|
||||||
>
|
|
||||||
<ListItemText
|
|
||||||
primary={suboption.name}
|
|
||||||
primaryTypographyProps={{
|
|
||||||
variant: 'subtitle1',
|
|
||||||
title: 'Property name',
|
|
||||||
sx: { fontWeight: 700 },
|
|
||||||
}}
|
|
||||||
secondary={suboption.value || ''}
|
|
||||||
secondaryTypographyProps={{
|
|
||||||
variant: 'subtitle2',
|
|
||||||
title: 'Sample value',
|
|
||||||
noWrap: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ListItemButton>
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
sx={{ py: 0.5 }}
|
||||||
|
>
|
||||||
|
<ListItemText primary={option.name} />
|
||||||
|
|
||||||
|
{!!option.output?.length &&
|
||||||
|
(current === index ? <ExpandLess /> : <ExpandMore />)}
|
||||||
|
</ListItemButton>
|
||||||
|
|
||||||
|
<Collapse in={current === index || filteredData.length === 1} timeout="auto" unmountOnExit>
|
||||||
|
<FixedSizeList
|
||||||
|
height={computeListHeight(getPartialArray((option.output as any) || [], listLength).length)}
|
||||||
|
width="100%"
|
||||||
|
itemSize={LIST_ITEM_HEIGHT}
|
||||||
|
itemCount={getPartialArray((option.output as any) || [], listLength).length}
|
||||||
|
overscanCount={2}
|
||||||
|
itemData={getPartialArray((option.output as any) || [], listLength)}
|
||||||
|
data-test="power-input-suggestion-group"
|
||||||
|
>
|
||||||
|
{renderItem}
|
||||||
|
</FixedSizeList>
|
||||||
|
|
||||||
|
{(option.output?.length || 0) > listLength && (
|
||||||
|
<Button fullWidth onClick={expandList}>
|
||||||
|
Show all
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</List>
|
|
||||||
|
|
||||||
{(option.output?.length || 0) > listLength && (
|
{listLength === Infinity && (
|
||||||
<Button fullWidth onClick={expandList}>
|
<Button fullWidth onClick={collapseList}>
|
||||||
Show all
|
Show less
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</Collapse>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
{listLength === Infinity && (
|
{filteredData.length === 0 && (
|
||||||
<Button fullWidth onClick={collapseList}>
|
<Typography sx={{ p: (theme) => theme.spacing(0, 0, 2, 2) }}>
|
||||||
Show less
|
{formatMessage('powerInputSuggestions.noOptions')}
|
||||||
</Button>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Collapse>
|
|
||||||
</>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -3,38 +3,63 @@ import type { IStep } from '@automatisch/types';
|
|||||||
const joinBy = (delimiter = '.', ...args: string[]) =>
|
const joinBy = (delimiter = '.', ...args: string[]) =>
|
||||||
args.filter(Boolean).join(delimiter);
|
args.filter(Boolean).join(delimiter);
|
||||||
|
|
||||||
const process = (data: any, parentKey?: any, index?: number): any[] => {
|
type TProcessPayload = {
|
||||||
|
data: any;
|
||||||
|
parentKey: string;
|
||||||
|
index?: number;
|
||||||
|
parentLabel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const process = ({ data, parentKey, index, parentLabel = '' }: TProcessPayload): any[] => {
|
||||||
if (typeof data !== 'object') {
|
if (typeof data !== 'object') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: `${parentKey}.${index}`,
|
label: `${parentLabel}.${index}`,
|
||||||
value: data,
|
value: `${parentKey}.${index}`,
|
||||||
|
sampleValue: data,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = Object.entries(data);
|
const entries = Object.entries(data);
|
||||||
|
|
||||||
return entries.flatMap(([name, value]) => {
|
return entries.flatMap(([name, sampleValue]) => {
|
||||||
const fullName = joinBy(
|
const label = joinBy(
|
||||||
|
'.',
|
||||||
|
parentLabel,
|
||||||
|
(index as number)?.toString(),
|
||||||
|
name
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = joinBy(
|
||||||
'.',
|
'.',
|
||||||
parentKey,
|
parentKey,
|
||||||
(index as number)?.toString(),
|
(index as number)?.toString(),
|
||||||
name
|
name
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(sampleValue)) {
|
||||||
return value.flatMap((item, index) => process(item, fullName, index));
|
return sampleValue.flatMap((item, index) => process({
|
||||||
|
data: item,
|
||||||
|
parentKey: value,
|
||||||
|
index,
|
||||||
|
parentLabel: label
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (typeof sampleValue === 'object' && sampleValue !== null) {
|
||||||
return process(value, fullName);
|
return process({
|
||||||
|
data: sampleValue,
|
||||||
|
parentKey: value,
|
||||||
|
parentLabel: label,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: fullName,
|
label,
|
||||||
value,
|
value,
|
||||||
|
sampleValue,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -52,12 +77,11 @@ export const processStepWithExecutions = (steps: IStep[]): any[] => {
|
|||||||
.map((step: IStep, index: number) => ({
|
.map((step: IStep, index: number) => ({
|
||||||
id: step.id,
|
id: step.id,
|
||||||
// TODO: replace with step.name once introduced
|
// TODO: replace with step.name once introduced
|
||||||
name: `${index + 1}. ${
|
name: `${index + 1}. ${(step.appKey || '').charAt(0)?.toUpperCase() + step.appKey?.slice(1)
|
||||||
(step.appKey || '').charAt(0)?.toUpperCase() + step.appKey?.slice(1)
|
}`,
|
||||||
}`,
|
output: process({
|
||||||
output: process(
|
data: step.executionSteps?.[0]?.dataOut || {},
|
||||||
step.executionSteps?.[0]?.dataOut || {},
|
parentKey: `step.${step.id}`,
|
||||||
`step.${step.id}`
|
}),
|
||||||
),
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@@ -1,25 +1,26 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
import Chip from '@mui/material/Chip';
|
|
||||||
import Popper from '@mui/material/Popper';
|
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import * as React from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { createEditor } from 'slate';
|
import { createEditor } from 'slate';
|
||||||
import { Slate, Editable, useSelected, useFocused } from 'slate-react';
|
import { Editable } from 'slate-react';
|
||||||
|
|
||||||
|
import Slate from 'components/Slate';
|
||||||
|
import Element from 'components/Slate/Element';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
serialize,
|
customizeEditor,
|
||||||
deserialize,
|
deserialize,
|
||||||
insertVariable,
|
insertVariable,
|
||||||
customizeEditor,
|
serialize,
|
||||||
} from './utils';
|
} from 'components/Slate/utils';
|
||||||
import Suggestions from './Suggestions';
|
|
||||||
import { StepExecutionsContext } from 'contexts/StepExecutions';
|
import { StepExecutionsContext } from 'contexts/StepExecutions';
|
||||||
|
|
||||||
import { FakeInput, InputLabelWrapper, ChildrenWrapper } from './style';
|
import { VariableElement } from 'components/Slate/types';
|
||||||
import { VariableElement } from './types';
|
import Popper from './Popper';
|
||||||
import { processStepWithExecutions } from './data';
|
import { processStepWithExecutions } from './data';
|
||||||
|
import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style';
|
||||||
|
|
||||||
type PowerInputProps = {
|
type PowerInputProps = {
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
@@ -59,6 +60,12 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
const [showVariableSuggestions, setShowVariableSuggestions] =
|
const [showVariableSuggestions, setShowVariableSuggestions] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
|
||||||
|
const disappearSuggestionsOnShift = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.code === 'Tab') {
|
||||||
|
setShowVariableSuggestions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const stepsWithVariables = React.useMemo(() => {
|
const stepsWithVariables = React.useMemo(() => {
|
||||||
return processStepWithExecutions(priorStepsWithExecutions);
|
return processStepWithExecutions(priorStepsWithExecutions);
|
||||||
}, [priorStepsWithExecutions]);
|
}, [priorStepsWithExecutions]);
|
||||||
@@ -93,7 +100,7 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
}) => (
|
}) => (
|
||||||
<Slate
|
<Slate
|
||||||
editor={editor}
|
editor={editor}
|
||||||
value={deserialize(value, stepsWithVariables)}
|
value={deserialize(value, [], stepsWithVariables)}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
controllerOnChange(serialize(value));
|
controllerOnChange(serialize(value));
|
||||||
}}
|
}}
|
||||||
@@ -122,6 +129,7 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
readOnly={disabled}
|
readOnly={disabled}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
|
onKeyDown={disappearSuggestionsOnShift}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
setShowVariableSuggestions(true);
|
setShowVariableSuggestions(true);
|
||||||
}}
|
}}
|
||||||
@@ -134,14 +142,14 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
{/* ghost placer for the variables popover */}
|
{/* ghost placer for the variables popover */}
|
||||||
<div ref={editorRef} style={{ position: 'absolute', right: 16, left: 16 }} />
|
<div ref={editorRef} style={{ position: 'absolute', right: 16, left: 16 }} />
|
||||||
|
|
||||||
<FormHelperText variant="outlined">{description}</FormHelperText>
|
<Popper
|
||||||
|
|
||||||
<SuggestionsPopper
|
|
||||||
open={showVariableSuggestions}
|
open={showVariableSuggestions}
|
||||||
anchorEl={editorRef.current}
|
anchorEl={editorRef.current}
|
||||||
data={stepsWithVariables}
|
data={stepsWithVariables}
|
||||||
onSuggestionClick={handleVariableSuggestionClick}
|
onSuggestionClick={handleVariableSuggestionClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormHelperText variant="outlined">{description}</FormHelperText>
|
||||||
</ChildrenWrapper>
|
</ChildrenWrapper>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
</Slate>
|
</Slate>
|
||||||
@@ -150,60 +158,4 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SuggestionsPopper = (props: any) => {
|
|
||||||
const { open, anchorEl, data, onSuggestionClick } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popper
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
style={{ width: anchorEl?.clientWidth, zIndex: 1 }}
|
|
||||||
modifiers={[
|
|
||||||
{
|
|
||||||
name: 'flip',
|
|
||||||
enabled: false,
|
|
||||||
options: {
|
|
||||||
altBoundary: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
|
|
||||||
</Popper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Element = (props: any) => {
|
|
||||||
const { attributes, children, element } = props;
|
|
||||||
switch (element.type) {
|
|
||||||
case 'variable':
|
|
||||||
return <Variable {...props} />;
|
|
||||||
default:
|
|
||||||
return <p {...attributes}>{children}</p>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Variable = ({ attributes, children, element }: any) => {
|
|
||||||
const selected = useSelected();
|
|
||||||
const focused = useFocused();
|
|
||||||
const label = (
|
|
||||||
<>
|
|
||||||
{element.name}
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Chip
|
|
||||||
{...attributes}
|
|
||||||
component="span"
|
|
||||||
contentEditable={false}
|
|
||||||
style={{
|
|
||||||
boxShadow: selected && focused ? '0 0 0 2px #B4D5FF' : 'none',
|
|
||||||
}}
|
|
||||||
size="small"
|
|
||||||
label={label}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PowerInput;
|
export default PowerInput;
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import MuiTabs from '@mui/material/Tabs';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
export const ChildrenWrapper = styled('div')`
|
export const ChildrenWrapper = styled('div')`
|
||||||
@@ -18,27 +19,41 @@ export const FakeInput = styled('div', {
|
|||||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||||
}) <{ disabled?: boolean }>`
|
}) <{ disabled?: boolean }>`
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
min-height: 52px;
|
min-height: 56px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
padding: ${({ theme }) => theme.spacing(0, 1.75)};
|
padding: ${({ theme }) => theme.spacing(0, 10, 0, 1.75)};
|
||||||
border-radius: ${({ theme }) => theme.spacing(0.5)};
|
border-radius: ${({ theme }) => theme.spacing(0.5)};
|
||||||
border-color: rgba(0, 0, 0, 0.23);
|
border-color: rgba(0, 0, 0, 0.23);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
${({ disabled, theme }) =>
|
${({ disabled, theme }) =>
|
||||||
!!disabled &&
|
!!disabled && `
|
||||||
`
|
color: ${theme.palette.action.disabled};
|
||||||
color: ${theme.palette.action.disabled},
|
border-color: ${theme.palette.action.disabled};
|
||||||
border-color: ${theme.palette.action.disabled},
|
|
||||||
`}
|
`}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: ${({ theme }) => theme.palette.text.primary};
|
border-color: ${({ theme }) => theme.palette.text.primary};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within, &:focus {
|
||||||
border-color: ${({ theme }) => theme.palette.primary.main};
|
&:before {
|
||||||
border-width: 2px;
|
border-color: ${({ theme }) => theme.palette.primary.main};
|
||||||
|
border-radius: ${({ theme }) => theme.spacing(0.5)};
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 2px;
|
||||||
|
bottom: -2px;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
left: -2px;
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Tabs = styled(MuiTabs)`
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
|
||||||
|
`;
|
||||||
|
@@ -1,125 +0,0 @@
|
|||||||
import { Text, Descendant, Transforms } from 'slate';
|
|
||||||
import { withHistory } from 'slate-history';
|
|
||||||
import { withReact } from 'slate-react';
|
|
||||||
|
|
||||||
import type { CustomEditor, CustomElement, VariableElement } from './types';
|
|
||||||
|
|
||||||
function getStepPosition(
|
|
||||||
id: string,
|
|
||||||
stepsWithVariables: Record<string, unknown>[]
|
|
||||||
) {
|
|
||||||
const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => {
|
|
||||||
return stepWithVariables.id === id;
|
|
||||||
});
|
|
||||||
|
|
||||||
return stepIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function humanizeVariableName(
|
|
||||||
variableName: string,
|
|
||||||
stepsWithVariables: Record<string, unknown>[]
|
|
||||||
) {
|
|
||||||
const nameWithoutCurlies = variableName.replace(/{{|}}/g, '');
|
|
||||||
const stepId = nameWithoutCurlies.match(stepIdRegExp)?.[1] || '';
|
|
||||||
const stepPosition = getStepPosition(stepId, stepsWithVariables);
|
|
||||||
const humanizedVariableName = nameWithoutCurlies.replace(
|
|
||||||
`step.${stepId}.`,
|
|
||||||
`step${stepPosition}.`
|
|
||||||
);
|
|
||||||
|
|
||||||
return humanizedVariableName;
|
|
||||||
}
|
|
||||||
|
|
||||||
const variableRegExp = /({{.*?}})/;
|
|
||||||
const stepIdRegExp = /^step.([\da-zA-Z-]*)/;
|
|
||||||
export const deserialize = (
|
|
||||||
value: string,
|
|
||||||
stepsWithVariables: any[]
|
|
||||||
): Descendant[] => {
|
|
||||||
if (!value)
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
children: [{ text: '' }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return value.split('\n').map((line) => {
|
|
||||||
const nodes = line.split(variableRegExp);
|
|
||||||
|
|
||||||
if (nodes.length > 1) {
|
|
||||||
return {
|
|
||||||
type: 'paragraph',
|
|
||||||
children: nodes.map((node) => {
|
|
||||||
if (node.match(variableRegExp)) {
|
|
||||||
return {
|
|
||||||
type: 'variable',
|
|
||||||
name: humanizeVariableName(node, stepsWithVariables),
|
|
||||||
value: node,
|
|
||||||
children: [{ text: '' }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: node,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'paragraph',
|
|
||||||
children: [{ text: line }],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serialize = (value: Descendant[]): string => {
|
|
||||||
return value.map((node) => serializeNode(node)).join('\n');
|
|
||||||
};
|
|
||||||
|
|
||||||
const serializeNode = (node: CustomElement | Descendant): string => {
|
|
||||||
if (Text.isText(node)) {
|
|
||||||
return node.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'variable') {
|
|
||||||
return node.value as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.children.map((n) => serializeNode(n)).join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const withVariables = (editor: CustomEditor) => {
|
|
||||||
const { isInline, isVoid } = editor;
|
|
||||||
|
|
||||||
editor.isInline = (element: CustomElement) => {
|
|
||||||
return element.type === 'variable' ? true : isInline(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.isVoid = (element: CustomElement) => {
|
|
||||||
return element.type === 'variable' ? true : isVoid(element);
|
|
||||||
};
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const insertVariable = (
|
|
||||||
editor: CustomEditor,
|
|
||||||
variableData: Pick<VariableElement, 'name' | 'value'>,
|
|
||||||
stepsWithVariables: Record<string, unknown>[]
|
|
||||||
) => {
|
|
||||||
const variable: VariableElement = {
|
|
||||||
type: 'variable',
|
|
||||||
name: humanizeVariableName(variableData.name as string, stepsWithVariables),
|
|
||||||
value: `{{${variableData.name}}}`,
|
|
||||||
children: [{ text: '' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
Transforms.insertNodes(editor, variable);
|
|
||||||
Transforms.move(editor);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const customizeEditor = (editor: CustomEditor): CustomEditor => {
|
|
||||||
return withVariables(withReact(withHistory(editor)));
|
|
||||||
};
|
|
@@ -1,70 +1,18 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import get from 'lodash/get';
|
|
||||||
import set from 'lodash/set';
|
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import forIn from 'lodash/forIn';
|
|
||||||
import isPlainObject from 'lodash/isPlainObject';
|
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
import { IJSONObject } from '@automatisch/types';
|
import { IJSONObject } from '@automatisch/types';
|
||||||
import JSONViewer from 'components/JSONViewer';
|
import JSONViewer from 'components/JSONViewer';
|
||||||
import SearchInput from 'components/SearchInput';
|
import SearchInput from 'components/SearchInput';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
import filterObject from 'helpers/filterObject';
|
||||||
|
|
||||||
type JSONViewerProps = {
|
type JSONViewerProps = {
|
||||||
data: IJSONObject;
|
data: IJSONObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
function aggregate(
|
|
||||||
data: any,
|
|
||||||
searchTerm: string,
|
|
||||||
result = {},
|
|
||||||
prefix: string[] = [],
|
|
||||||
withinArray = false
|
|
||||||
) {
|
|
||||||
if (withinArray) {
|
|
||||||
const containerValue = get(result, prefix, []);
|
|
||||||
|
|
||||||
result = aggregate(
|
|
||||||
data,
|
|
||||||
searchTerm,
|
|
||||||
result,
|
|
||||||
prefix.concat(containerValue.length.toString())
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(data)) {
|
|
||||||
forIn(data, (value, key) => {
|
|
||||||
const fullKey = [...prefix, key];
|
|
||||||
|
|
||||||
if (key.toLowerCase().includes(searchTerm)) {
|
|
||||||
set(result, fullKey, value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = aggregate(value, searchTerm, result, fullKey);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
forIn(data, (value) => {
|
|
||||||
result = aggregate(value, searchTerm, result, prefix, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
['string', 'number'].includes(typeof data) &&
|
|
||||||
String(data).toLowerCase().includes(searchTerm)
|
|
||||||
) {
|
|
||||||
set(result, prefix, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchableJSONViewer = ({ data }: JSONViewerProps) => {
|
const SearchableJSONViewer = ({ data }: JSONViewerProps) => {
|
||||||
const [filteredData, setFilteredData] = React.useState<IJSONObject | null>(
|
const [filteredData, setFilteredData] = React.useState<IJSONObject | null>(
|
||||||
data
|
data
|
||||||
@@ -81,7 +29,7 @@ const SearchableJSONViewer = ({ data }: JSONViewerProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFilteredData = aggregate(data, search);
|
const newFilteredData = filterObject(data, search);
|
||||||
|
|
||||||
if (isEmpty(newFilteredData)) {
|
if (isEmpty(newFilteredData)) {
|
||||||
setFilteredData(null);
|
setFilteredData(null);
|
||||||
|
12
packages/web/src/components/Slate/Element.tsx
Normal file
12
packages/web/src/components/Slate/Element.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Variable from './Variable';
|
||||||
|
|
||||||
|
export default function Element(props: any) {
|
||||||
|
const { attributes, children, element, disabled } = props;
|
||||||
|
|
||||||
|
switch (element.type) {
|
||||||
|
case 'variable':
|
||||||
|
return <Variable {...props} disabled={disabled} />;
|
||||||
|
default:
|
||||||
|
return <p {...attributes}>{children}</p>;
|
||||||
|
}
|
||||||
|
};
|
28
packages/web/src/components/Slate/Variable.tsx
Normal file
28
packages/web/src/components/Slate/Variable.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import { useSelected, useFocused } from 'slate-react';
|
||||||
|
|
||||||
|
export default function Variable({ attributes, children, element, disabled }: any) {
|
||||||
|
const selected = useSelected();
|
||||||
|
const focused = useFocused();
|
||||||
|
const label = (
|
||||||
|
<>
|
||||||
|
<span style={{ fontWeight: 500 }}>{element.name}</span>: <span style={{ fontWeight: 300 }}>{element.sampleValue}</span>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
{...attributes}
|
||||||
|
disabled={disabled}
|
||||||
|
component="span"
|
||||||
|
contentEditable={false}
|
||||||
|
style={{
|
||||||
|
boxShadow: selected && focused ? '0 0 0 2px #B4D5FF' : 'none',
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
3
packages/web/src/components/Slate/index.tsx
Normal file
3
packages/web/src/components/Slate/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Slate } from 'slate-react';
|
||||||
|
|
||||||
|
export default Slate;
|
@@ -5,14 +5,21 @@ export type VariableElement = {
|
|||||||
type: 'variable';
|
type: 'variable';
|
||||||
value?: unknown;
|
value?: unknown;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
sampleValue?: unknown;
|
||||||
children: Text[];
|
children: Text[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParagraphElement = {
|
export type ParagraphElement = {
|
||||||
type: 'paragraph';
|
type: 'paragraph';
|
||||||
|
value?: string;
|
||||||
children: Descendant[];
|
children: Descendant[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CustomText = {
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type CustomEditor = BaseEditor & ReactEditor;
|
export type CustomEditor = BaseEditor & ReactEditor;
|
||||||
|
|
||||||
export type CustomElement = VariableElement | ParagraphElement;
|
export type CustomElement = VariableElement | ParagraphElement;
|
280
packages/web/src/components/Slate/utils.ts
Normal file
280
packages/web/src/components/Slate/utils.ts
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import { Text, Descendant } from 'slate';
|
||||||
|
import { withHistory } from 'slate-history';
|
||||||
|
import { ReactEditor, withReact } from 'slate-react';
|
||||||
|
import { IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
|
||||||
|
import type { CustomEditor, CustomElement, CustomText, ParagraphElement, VariableElement } from './types';
|
||||||
|
|
||||||
|
type StepWithVariables = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
output: {
|
||||||
|
label: string;
|
||||||
|
sampleValue: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type StepsWithVariables = StepWithVariables[];
|
||||||
|
|
||||||
|
function isCustomText(value: any): value is CustomText {
|
||||||
|
const isText = Text.isText(value);
|
||||||
|
const hasValueProperty = 'value' in value;
|
||||||
|
|
||||||
|
if (isText && hasValueProperty) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStepPosition(
|
||||||
|
id: string,
|
||||||
|
stepsWithVariables: StepsWithVariables
|
||||||
|
) {
|
||||||
|
const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => {
|
||||||
|
return stepWithVariables.id === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
return stepIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariableName(variable: string) {
|
||||||
|
return variable.replace(/{{|}}/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariableStepId(variable: string) {
|
||||||
|
const nameWithoutCurlies = getVariableName(variable);
|
||||||
|
const stepId = nameWithoutCurlies.match(stepIdRegExp)?.[1] || '';
|
||||||
|
|
||||||
|
return stepId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariableSampleValue(variable: string, stepsWithVariables: StepsWithVariables) {
|
||||||
|
const variableStepId = getVariableStepId(variable);
|
||||||
|
const stepWithVariables = stepsWithVariables.find(({ id }: { id: string }) => id === variableStepId);
|
||||||
|
|
||||||
|
if (!stepWithVariables) return null;
|
||||||
|
|
||||||
|
const variableName = getVariableName(variable);
|
||||||
|
const variableData = stepWithVariables.output.find(({ value }) => variableName === value);
|
||||||
|
|
||||||
|
if (!variableData) return null;
|
||||||
|
|
||||||
|
return variableData.sampleValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariableDetails(variable: string, stepsWithVariables: StepsWithVariables) {
|
||||||
|
const variableName = getVariableName(variable);
|
||||||
|
const stepId = getVariableStepId(variableName);
|
||||||
|
const stepPosition = getStepPosition(stepId, stepsWithVariables);
|
||||||
|
const sampleValue = getVariableSampleValue(variable, stepsWithVariables);
|
||||||
|
const label = variableName.replace(
|
||||||
|
`step.${stepId}.`,
|
||||||
|
`step${stepPosition}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sampleValue,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableRegExp = /({{.*?}})/;
|
||||||
|
const stepIdRegExp = /^step.([\da-zA-Z-]*)/;
|
||||||
|
|
||||||
|
export const deserialize = (
|
||||||
|
value: string,
|
||||||
|
options: readonly IFieldDropdownOption[],
|
||||||
|
stepsWithVariables: StepsWithVariables
|
||||||
|
): Descendant[] => {
|
||||||
|
if (!value)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [{ text: '' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedNativeOption = options.find((option) => value === option.value);
|
||||||
|
|
||||||
|
if (selectedNativeOption) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
value: selectedNativeOption.value as string,
|
||||||
|
children: [{ text: selectedNativeOption.label }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.split('\n').map((line) => {
|
||||||
|
const nodes = line.split(variableRegExp);
|
||||||
|
|
||||||
|
if (nodes.length > 1) {
|
||||||
|
return {
|
||||||
|
type: 'paragraph',
|
||||||
|
children: nodes.map((node) => {
|
||||||
|
if (node.match(variableRegExp)) {
|
||||||
|
const variableDetails = getVariableDetails(node, stepsWithVariables);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'variable',
|
||||||
|
name: variableDetails.label,
|
||||||
|
sampleValue: variableDetails.sampleValue,
|
||||||
|
value: node,
|
||||||
|
children: [{ text: '' }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: node,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [{ text: line }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serialize = (value: Descendant[]): string => {
|
||||||
|
const serializedNodes = value.map((node) => serializeNode(node));
|
||||||
|
|
||||||
|
const hasSingleNode = value.length === 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return single serialize node alone so that we don't stringify.
|
||||||
|
* booleans stay booleans, numbers stay number
|
||||||
|
*/
|
||||||
|
if (hasSingleNode) {
|
||||||
|
return serializedNodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const serializedValue = serializedNodes.join('\n');
|
||||||
|
return serializedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeNode = (node: CustomElement | Descendant): string => {
|
||||||
|
if (isCustomText(node)) return node.value;
|
||||||
|
|
||||||
|
if (Text.isText(node)) {
|
||||||
|
return node.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'variable') {
|
||||||
|
return node.value as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSingleChild = node.children.length === 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* serialize alone so that we don't stringify.
|
||||||
|
* booleans stay booleans, numbers stay number
|
||||||
|
*/
|
||||||
|
if (hasSingleChild) {
|
||||||
|
return serializeNode(node.children[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.children.map((n) => serializeNode(n)).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withVariables = (editor: CustomEditor) => {
|
||||||
|
const { isInline, isVoid } = editor;
|
||||||
|
|
||||||
|
editor.isInline = (element: CustomElement) => {
|
||||||
|
return element.type === 'variable' ? true : isInline(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.isVoid = (element: CustomElement) => {
|
||||||
|
return element.type === 'variable' ? true : isVoid(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertVariable = (
|
||||||
|
editor: CustomEditor,
|
||||||
|
variableData: Record<string, unknown>,
|
||||||
|
stepsWithVariables: StepsWithVariables
|
||||||
|
) => {
|
||||||
|
const variableDetails = getVariableDetails(`{{${variableData.value}}}`, stepsWithVariables);
|
||||||
|
|
||||||
|
const variable: VariableElement = {
|
||||||
|
type: 'variable',
|
||||||
|
name: variableDetails.label,
|
||||||
|
sampleValue: variableDetails.sampleValue,
|
||||||
|
value: `{{${variableData.value}}}`,
|
||||||
|
children: [{ text: '' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.insertNodes(variable, { select: false });
|
||||||
|
|
||||||
|
focusEditor(editor);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const focusEditor = (editor: CustomEditor) => {
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
editor.move();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetEditor = (editor: CustomEditor, options?: { focus: boolean }) => {
|
||||||
|
const focus = options?.focus || false;
|
||||||
|
|
||||||
|
editor.removeNodes({
|
||||||
|
at: {
|
||||||
|
anchor: editor.start([]),
|
||||||
|
focus: editor.end([])
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// `editor.normalize({ force: true })` doesn't add an empty node in the editor
|
||||||
|
editor.insertNode(createTextNode(''));
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
focusEditor(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const overrideEditorValue = (editor: CustomEditor, options: { option: IFieldDropdownOption, focus: boolean }) => {
|
||||||
|
const { option, focus } = options;
|
||||||
|
|
||||||
|
const variable: ParagraphElement = {
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: option.value as string,
|
||||||
|
text: option.label as string
|
||||||
|
}
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
editor.withoutNormalizing(() => {
|
||||||
|
editor.removeNodes({
|
||||||
|
at: {
|
||||||
|
anchor: editor.start([]),
|
||||||
|
focus: editor.end([])
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.insertNode(variable);
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
focusEditor(editor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTextNode = (text: string): ParagraphElement => ({
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export const customizeEditor = (editor: CustomEditor): CustomEditor => {
|
||||||
|
return withVariables(withReact(withHistory(editor)));
|
||||||
|
};
|
@@ -8,7 +8,9 @@ export const CREATE_STEP = gql`
|
|||||||
key
|
key
|
||||||
appKey
|
appKey
|
||||||
parameters
|
parameters
|
||||||
|
iconUrl
|
||||||
position
|
position
|
||||||
|
webhookUrl
|
||||||
status
|
status
|
||||||
connection {
|
connection {
|
||||||
id
|
id
|
||||||
|
53
packages/web/src/helpers/filterObject.ts
Normal file
53
packages/web/src/helpers/filterObject.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import get from 'lodash/get';
|
||||||
|
import set from 'lodash/set';
|
||||||
|
import forIn from 'lodash/forIn';
|
||||||
|
import isPlainObject from 'lodash/isPlainObject';
|
||||||
|
|
||||||
|
export default function filterObject(
|
||||||
|
data: any,
|
||||||
|
searchTerm: string,
|
||||||
|
result = {},
|
||||||
|
prefix: string[] = [],
|
||||||
|
withinArray = false
|
||||||
|
) {
|
||||||
|
if (withinArray) {
|
||||||
|
const containerValue = get(result, prefix, []);
|
||||||
|
|
||||||
|
result = filterObject(
|
||||||
|
data,
|
||||||
|
searchTerm,
|
||||||
|
result,
|
||||||
|
prefix.concat(containerValue.length.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPlainObject(data)) {
|
||||||
|
forIn(data, (value, key) => {
|
||||||
|
const fullKey = [...prefix, key];
|
||||||
|
|
||||||
|
if (key.toLowerCase().includes(searchTerm)) {
|
||||||
|
set(result, fullKey, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = filterObject(value, searchTerm, result, fullKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
forIn(data, (value) => {
|
||||||
|
result = filterObject(value, searchTerm, result, prefix, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['string', 'number'].includes(typeof data) &&
|
||||||
|
String(data).toLowerCase().includes(searchTerm)
|
||||||
|
) {
|
||||||
|
set(result, prefix, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
16
packages/web/src/helpers/isEmpty.ts
Normal file
16
packages/web/src/helpers/isEmpty.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import lodashIsEmpty from 'lodash/isEmpty';
|
||||||
|
|
||||||
|
export default function isEmpty(value: any) {
|
||||||
|
if (value === undefined && value === null) return true;
|
||||||
|
|
||||||
|
if (Array.isArray(value) || typeof value === 'string') {
|
||||||
|
return value.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isNaN(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// covers objects and anything else possibly
|
||||||
|
return lodashIsEmpty(value);
|
||||||
|
};
|
@@ -162,5 +162,7 @@
|
|||||||
"trialBadge.over": "Trial is over",
|
"trialBadge.over": "Trial is over",
|
||||||
"trialOverAlert.text": "Your free trial is over. Please <link>upgrade</link> your plan to continue using Automatisch.",
|
"trialOverAlert.text": "Your free trial is over. Please <link>upgrade</link> your plan to continue using Automatisch.",
|
||||||
"checkoutCompletedAlert.text": "Thank you for upgrading your subscription and supporting our self-funded business!",
|
"checkoutCompletedAlert.text": "Thank you for upgrading your subscription and supporting our self-funded business!",
|
||||||
"subscriptionCancelledAlert.text": "Your subscription is cancelled, but you can continue using Automatisch until {date}."
|
"subscriptionCancelledAlert.text": "Your subscription is cancelled, but you can continue using Automatisch until {date}.",
|
||||||
|
"customAutocomplete.noOptions": "No options available.",
|
||||||
|
"powerInputSuggestions.noOptions": "No options available."
|
||||||
}
|
}
|
||||||
|
57
yarn.lock
57
yarn.lock
@@ -1315,6 +1315,13 @@
|
|||||||
core-js-pure "^3.20.2"
|
core-js-pure "^3.20.2"
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.0.0":
|
||||||
|
version "7.21.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
|
||||||
|
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.11"
|
||||||
|
|
||||||
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||||
version "7.16.7"
|
version "7.16.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
|
||||||
@@ -2121,6 +2128,11 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.0.3"
|
"@jridgewell/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||||
|
|
||||||
|
"@juggle/resize-observer@^3.4.0":
|
||||||
|
version "3.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||||
|
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||||
|
|
||||||
"@lerna/add@4.0.0":
|
"@lerna/add@4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f"
|
resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f"
|
||||||
@@ -4178,6 +4190,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-window@^1.8.5":
|
||||||
|
version "1.8.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
|
||||||
|
integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@16 || 17", "@types/react@^17.0.0":
|
"@types/react@*", "@types/react@16 || 17", "@types/react@^17.0.0":
|
||||||
version "17.0.38"
|
version "17.0.38"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
|
||||||
@@ -11978,6 +11997,11 @@ memfs@^3.1.2, memfs@^3.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fs-monkey "1.0.3"
|
fs-monkey "1.0.3"
|
||||||
|
|
||||||
|
"memoize-one@>=3.1.1 <6":
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||||
|
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||||
|
|
||||||
memory-cache@^0.2.0:
|
memory-cache@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a"
|
resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a"
|
||||||
@@ -14770,6 +14794,14 @@ react-transition-group@^4.4.5:
|
|||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
react-window@^1.8.9:
|
||||||
|
version "1.8.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.9.tgz#24bc346be73d0468cdf91998aac94e32bc7fa6a8"
|
||||||
|
integrity sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.0.0"
|
||||||
|
memoize-one ">=3.1.1 <6"
|
||||||
|
|
||||||
react@^17.0.2:
|
react@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
@@ -15722,18 +15754,19 @@ slash@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
||||||
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
||||||
|
|
||||||
slate-history@^0.66.0:
|
slate-history@^0.93.0:
|
||||||
version "0.66.0"
|
version "0.93.0"
|
||||||
resolved "https://registry.yarnpkg.com/slate-history/-/slate-history-0.66.0.tgz#ac63fddb903098ceb4c944433e3f75fe63acf940"
|
resolved "https://registry.yarnpkg.com/slate-history/-/slate-history-0.93.0.tgz#d2fad47e4e8b262ab7c86b653f5dd6d9b6d85277"
|
||||||
integrity sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==
|
integrity sha512-Gr1GMGPipRuxIz41jD2/rbvzPj8eyar56TVMyJBvBeIpQSSjNISssvGNDYfJlSWM8eaRqf6DAcxMKzsLCYeX6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
is-plain-object "^5.0.0"
|
is-plain-object "^5.0.0"
|
||||||
|
|
||||||
slate-react@^0.72.9:
|
slate-react@^0.94.2:
|
||||||
version "0.72.9"
|
version "0.94.2"
|
||||||
resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.72.9.tgz#b05dd533bd29dd2d4796b614a8d8e01f214bb714"
|
resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.94.2.tgz#3fc70f0212f42a1c417012d7a911f0ec9f6b11fe"
|
||||||
integrity sha512-FEsqB+D1R/h+w1eCtHH367Krw2X7vju2GjMRL/d0bUiCRXlV50J9I9TJizvi7aaZyqBY8BypCuIiq9nNmsulCA==
|
integrity sha512-4wDSuTuGBkdQ609CS55uc2Yhfa5but21usBgAtCVhPJQazL85kzN2vUUYTmGb7d/mpP9tdnJiVPopIyhqlRJ8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@juggle/resize-observer" "^3.4.0"
|
||||||
"@types/is-hotkey" "^0.1.1"
|
"@types/is-hotkey" "^0.1.1"
|
||||||
"@types/lodash" "^4.14.149"
|
"@types/lodash" "^4.14.149"
|
||||||
direction "^1.0.3"
|
direction "^1.0.3"
|
||||||
@@ -15743,10 +15776,10 @@ slate-react@^0.72.9:
|
|||||||
scroll-into-view-if-needed "^2.2.20"
|
scroll-into-view-if-needed "^2.2.20"
|
||||||
tiny-invariant "1.0.6"
|
tiny-invariant "1.0.6"
|
||||||
|
|
||||||
slate@^0.72.8:
|
slate@^0.94.1:
|
||||||
version "0.72.8"
|
version "0.94.1"
|
||||||
resolved "https://registry.yarnpkg.com/slate/-/slate-0.72.8.tgz#5a018edf24e45448655293a68bfbcf563aa5ba81"
|
resolved "https://registry.yarnpkg.com/slate/-/slate-0.94.1.tgz#13b0ba7d0a7eeb0ec89a87598e9111cbbd685696"
|
||||||
integrity sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==
|
integrity sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==
|
||||||
dependencies:
|
dependencies:
|
||||||
immer "^9.0.6"
|
immer "^9.0.6"
|
||||||
is-plain-object "^5.0.0"
|
is-plain-object "^5.0.0"
|
||||||
|
Reference in New Issue
Block a user