feat: add schedule integration
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"name": "@automatisch/root",
|
"name": "@automatisch/root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "lerna run --stream --parallel --scope=@*/{web,docs,backend} dev",
|
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||||
"start:web": "lerna run --stream --scope=@*/web dev",
|
"start:web": "lerna run --stream --scope=@*/web dev",
|
||||||
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
||||||
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} lint",
|
"lint": "lerna run --no-bail --stream --parallel --scope=@*/{web,backend,cli} lint",
|
||||||
|
@@ -219,7 +219,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New favorite photo",
|
"name": "New favorite photo",
|
||||||
"key": "newFavoritePhoto",
|
"key": "newFavoritePhoto",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when you favorite a photo.",
|
"description": "Triggers when you favorite a photo.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -235,7 +234,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New photo in album",
|
"name": "New photo in album",
|
||||||
"key": "newPhotoInAlbum",
|
"key": "newPhotoInAlbum",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when you add a new photo in an album.",
|
"description": "Triggers when you add a new photo in an album.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -274,7 +272,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New photo",
|
"name": "New photo",
|
||||||
"key": "newPhoto",
|
"key": "newPhoto",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when you add a new photo.",
|
"description": "Triggers when you add a new photo.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -290,7 +287,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New album",
|
"name": "New album",
|
||||||
"key": "newAlbum",
|
"key": "newAlbum",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when you create a new album.",
|
"description": "Triggers when you create a new album.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
|
@@ -219,7 +219,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New repository",
|
"name": "New repository",
|
||||||
"key": "newRepository",
|
"key": "newRepository",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new repository is created",
|
"description": "Triggers when a new repository is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -235,7 +234,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New organization",
|
"name": "New organization",
|
||||||
"key": "newOrganization",
|
"key": "newOrganization",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new organization is created",
|
"description": "Triggers when a new organization is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -251,7 +249,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New branch",
|
"name": "New branch",
|
||||||
"key": "newBranch",
|
"key": "newBranch",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new branch is created",
|
"description": "Triggers when a new branch is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -290,7 +287,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New notification",
|
"name": "New notification",
|
||||||
"key": "newNotification",
|
"key": "newNotification",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new notification is created",
|
"description": "Triggers when a new notification is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -330,7 +326,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New pull request",
|
"name": "New pull request",
|
||||||
"key": "newPullRequest",
|
"key": "newPullRequest",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new pull request is created",
|
"description": "Triggers when a new pull request is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -369,7 +364,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New watcher",
|
"name": "New watcher",
|
||||||
"key": "newWatcher",
|
"key": "newWatcher",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new watcher is added to a repo",
|
"description": "Triggers when a new watcher is added to a repo",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -408,7 +402,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New milestone",
|
"name": "New milestone",
|
||||||
"key": "newMilestone",
|
"key": "newMilestone",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new milestone is created",
|
"description": "Triggers when a new milestone is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -447,7 +440,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New commit comment",
|
"name": "New commit comment",
|
||||||
"key": "newCommitComment",
|
"key": "newCommitComment",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new commit comment is created",
|
"description": "Triggers when a new commit comment is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -486,7 +478,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New label",
|
"name": "New label",
|
||||||
"key": "newLabel",
|
"key": "newLabel",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new label is created",
|
"description": "Triggers when a new label is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -525,7 +516,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New collaborator",
|
"name": "New collaborator",
|
||||||
"key": "newCollaborator",
|
"key": "newCollaborator",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new collaborator is added to a repo",
|
"description": "Triggers when a new collaborator is added to a repo",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -564,7 +554,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New release",
|
"name": "New release",
|
||||||
"key": "newRelease",
|
"key": "newRelease",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new release is created",
|
"description": "Triggers when a new release is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
@@ -603,7 +592,6 @@
|
|||||||
{
|
{
|
||||||
"name": "New commit",
|
"name": "New commit",
|
||||||
"key": "newCommit",
|
"key": "newCommit",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Triggers when a new commit is created",
|
"description": "Triggers when a new commit is created",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
|
1
packages/backend/src/apps/schedule/assets/favicon.svg
Normal file
1
packages/backend/src/apps/schedule/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" viewBox="0 0 24 24"><path fill="#1976d2" d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"></path><path fill="#1976d2" d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"></path></svg>
|
After Width: | Height: | Size: 345 B |
0
packages/backend/src/apps/schedule/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/schedule/index.d.ts
vendored
Normal file
18
packages/backend/src/apps/schedule/index.ts
Normal file
18
packages/backend/src/apps/schedule/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Triggers from './triggers';
|
||||||
|
import {
|
||||||
|
IService,
|
||||||
|
IApp,
|
||||||
|
IJSONObject,
|
||||||
|
} from '@automatisch/types';
|
||||||
|
|
||||||
|
export default class Schedule implements IService {
|
||||||
|
triggers: Triggers;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
appData: IApp,
|
||||||
|
connectionData: IJSONObject,
|
||||||
|
parameters: IJSONObject
|
||||||
|
) {
|
||||||
|
this.triggers = new Triggers(connectionData, parameters);
|
||||||
|
}
|
||||||
|
}
|
46
packages/backend/src/apps/schedule/info.json
Normal file
46
packages/backend/src/apps/schedule/info.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "Schedule",
|
||||||
|
"key": "schedule",
|
||||||
|
"iconUrl": "{BASE_URL}/apps/schedule/assets/favicon.svg",
|
||||||
|
"docUrl": "https://automatisch.io/docs/schedule",
|
||||||
|
"primaryColor": "0059F7",
|
||||||
|
"requiresAuthentication": false,
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"name": "Every hour",
|
||||||
|
"key": "everyHour",
|
||||||
|
"description": "Triggers every hour.",
|
||||||
|
"substeps": [
|
||||||
|
{
|
||||||
|
"key": "chooseTrigger",
|
||||||
|
"name": "Set up a trigger",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"label": "Trigger on weekends?",
|
||||||
|
"key": "triggersOnWeekend",
|
||||||
|
"type": "dropdown",
|
||||||
|
"description": "Should this flow trigger on Saturday and Sunday?",
|
||||||
|
"required": true,
|
||||||
|
"value": true,
|
||||||
|
"variables": false,
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Yes",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "No",
|
||||||
|
"value": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "testStep",
|
||||||
|
"name": "Test trigger"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
10
packages/backend/src/apps/schedule/triggers.ts
Normal file
10
packages/backend/src/apps/schedule/triggers.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import EveryHour from './triggers/every-hour';
|
||||||
|
|
||||||
|
export default class Triggers {
|
||||||
|
everyHour: EveryHour;
|
||||||
|
|
||||||
|
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||||
|
this.everyHour = new EveryHour(parameters);
|
||||||
|
}
|
||||||
|
}
|
35
packages/backend/src/apps/schedule/triggers/every-hour.ts
Normal file
35
packages/backend/src/apps/schedule/triggers/every-hour.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import type { IJSONObject, IJSONValue, ITrigger } from '@automatisch/types';
|
||||||
|
import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
|
||||||
|
|
||||||
|
export default class EveryHour implements ITrigger {
|
||||||
|
triggersOnWeekend?: boolean | string;
|
||||||
|
|
||||||
|
constructor(parameters: IJSONObject) {
|
||||||
|
if (parameters.triggersOnWeekend) {
|
||||||
|
this.triggersOnWeekend = parameters.triggersOnWeekend as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get interval() {
|
||||||
|
if (this.triggersOnWeekend) {
|
||||||
|
return cronTimes.everyHour;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronTimes.everyHourExcludingWeekends;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(startDateTime: Date) {
|
||||||
|
const dateTime = DateTime.fromJSDate(startDateTime);
|
||||||
|
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
|
||||||
|
|
||||||
|
return [dateTimeObjectRepresentation] as IJSONValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async testRun() {
|
||||||
|
const nextCronDateTime = getNextCronDateTime(this.interval);
|
||||||
|
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
|
||||||
|
|
||||||
|
return [dateTimeObjectRepresentation] as IJSONValue;
|
||||||
|
}
|
||||||
|
}
|
28
packages/backend/src/apps/schedule/utils.ts
Normal file
28
packages/backend/src/apps/schedule/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import cronParser from 'cron-parser';
|
||||||
|
|
||||||
|
export const cronTimes = {
|
||||||
|
everyHour: '0 * * * *',
|
||||||
|
everyHourExcludingWeekends: '0 * * * 1-5',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getNextCronDateTime(cronString: string) {
|
||||||
|
const cronDate = cronParser.parseExpression(cronString);
|
||||||
|
const matchingNextCronDateTime = cronDate.next();
|
||||||
|
const matchingNextDateTime = DateTime.fromJSDate(matchingNextCronDateTime.toDate());
|
||||||
|
|
||||||
|
return matchingNextDateTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDateTimeObjectRepresentation(dateTime: DateTime) {
|
||||||
|
const defaults = dateTime.toObject();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
ISO_date_time: dateTime.toISO(),
|
||||||
|
pretty_date: dateTime.toLocaleString(DateTime.DATE_MED),
|
||||||
|
pretty_time: dateTime.toLocaleString(DateTime.TIME_WITH_SECONDS),
|
||||||
|
pretty_day_of_week: dateTime.toFormat('cccc'),
|
||||||
|
day_of_week: dateTime.weekday,
|
||||||
|
};
|
||||||
|
}
|
@@ -219,7 +219,6 @@
|
|||||||
{
|
{
|
||||||
"name": "My Tweet",
|
"name": "My Tweet",
|
||||||
"key": "myTweet",
|
"key": "myTweet",
|
||||||
"interval": "15m",
|
|
||||||
"description": "Will be triggered when you tweet something new.",
|
"description": "Will be triggered when you tweet something new.",
|
||||||
"substeps": [
|
"substeps": [
|
||||||
{
|
{
|
||||||
|
@@ -9,9 +9,7 @@ type Params = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const JOB_NAME = 'processorJob';
|
const JOB_NAME = 'processorJob';
|
||||||
const REPEAT_OPTIONS = {
|
const EVERY_15_MINUTES_CRON = '*/15 * * * *';
|
||||||
every: 60000, // 1 minute
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFlowStatus = async (
|
const updateFlowStatus = async (
|
||||||
_parent: unknown,
|
_parent: unknown,
|
||||||
@@ -33,17 +31,27 @@ const updateFlowStatus = async (
|
|||||||
active: params.input.active,
|
active: params.input.active,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const triggerStep = await flow.getTriggerStep();
|
||||||
|
const trigger = await triggerStep.getTrigger();
|
||||||
|
const interval = trigger.interval;
|
||||||
|
const repeatOptions = {
|
||||||
|
cron: interval || EVERY_15_MINUTES_CRON,
|
||||||
|
}
|
||||||
|
|
||||||
if (flow.active) {
|
if (flow.active) {
|
||||||
await processorQueue.add(
|
await processorQueue.add(
|
||||||
JOB_NAME,
|
JOB_NAME,
|
||||||
{ flowId: flow.id },
|
{ flowId: flow.id },
|
||||||
{
|
{
|
||||||
repeat: REPEAT_OPTIONS,
|
repeat: repeatOptions,
|
||||||
jobId: flow.id,
|
jobId: flow.id,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await processorQueue.removeRepeatable(JOB_NAME, REPEAT_OPTIONS, flow.id);
|
const repeatableJobs = await processorQueue.getRepeatableJobs();
|
||||||
|
const job = repeatableJobs.find(job => job.id === flow.id);
|
||||||
|
|
||||||
|
await processorQueue.removeRepeatableByKey(job.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return flow;
|
return flow;
|
||||||
|
@@ -374,6 +374,7 @@ type TriggerSubstepArgument {
|
|||||||
variables: Boolean
|
variables: Boolean
|
||||||
source: TriggerSubstepArgumentSource
|
source: TriggerSubstepArgumentSource
|
||||||
dependsOn: [String]
|
dependsOn: [String]
|
||||||
|
options: [TriggerSubstepArgumentOption]
|
||||||
}
|
}
|
||||||
|
|
||||||
type TriggerSubstepArgumentSource {
|
type TriggerSubstepArgumentSource {
|
||||||
@@ -382,6 +383,11 @@ type TriggerSubstepArgumentSource {
|
|||||||
arguments: [TriggerSubstepArgumentSourceArgument]
|
arguments: [TriggerSubstepArgumentSourceArgument]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TriggerSubstepArgumentOption {
|
||||||
|
label: String
|
||||||
|
value: JSONObject
|
||||||
|
}
|
||||||
|
|
||||||
type TriggerSubstepArgumentSourceArgument {
|
type TriggerSubstepArgumentSourceArgument {
|
||||||
name: String
|
name: String
|
||||||
value: String
|
value: String
|
||||||
|
@@ -82,6 +82,12 @@ class Flow extends Base {
|
|||||||
await super.$afterUpdate(opt, queryContext);
|
await super.$afterUpdate(opt, queryContext);
|
||||||
Telemetry.flowUpdated(this);
|
Telemetry.flowUpdated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTriggerStep(): Promise<Step> {
|
||||||
|
return await this.$relatedQuery('steps').findOne({
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Flow;
|
export default Flow;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { QueryContext, ModelOptions } from 'objection';
|
import { QueryContext, ModelOptions } from 'objection';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
import App from './app';
|
||||||
import Flow from './flow';
|
import Flow from './flow';
|
||||||
import Connection from './connection';
|
import Connection from './connection';
|
||||||
import ExecutionStep from './execution-step';
|
import ExecutionStep from './execution-step';
|
||||||
@@ -75,6 +76,27 @@ class Step extends Base {
|
|||||||
await super.$afterUpdate(opt, queryContext);
|
await super.$afterUpdate(opt, queryContext);
|
||||||
Telemetry.stepUpdated(this);
|
Telemetry.stepUpdated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isTrigger(): boolean {
|
||||||
|
return this.type === 'trigger';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTrigger() {
|
||||||
|
if (!this.isTrigger) return null;
|
||||||
|
|
||||||
|
const { appKey, connection, key, parameters = {} } = this;
|
||||||
|
|
||||||
|
const appData = App.findOneByKey(appKey);
|
||||||
|
const AppClass = (await import(`../apps/${appKey}`)).default;
|
||||||
|
const appInstance = new AppClass(
|
||||||
|
appData,
|
||||||
|
connection?.formattedData,
|
||||||
|
parameters,
|
||||||
|
);
|
||||||
|
const command = appInstance.triggers[key];
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Step;
|
export default Step;
|
||||||
|
16
packages/types/index.d.ts
vendored
16
packages/types/index.d.ts
vendored
@@ -86,6 +86,7 @@ export interface IFieldDropdown {
|
|||||||
name: string;
|
name: string;
|
||||||
variables: boolean;
|
variables: boolean;
|
||||||
dependsOn: string[];
|
dependsOn: string[];
|
||||||
|
options: IFieldDropdownOption[];
|
||||||
source: {
|
source: {
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -96,6 +97,11 @@ export interface IFieldDropdown {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFieldDropdownOption {
|
||||||
|
label: string;
|
||||||
|
value: boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IFieldText {
|
export interface IFieldText {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -146,7 +152,15 @@ export interface IApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IService {
|
export interface IService {
|
||||||
authenticationClient: IAuthentication;
|
authenticationClient?: IAuthentication;
|
||||||
|
triggers?: any;
|
||||||
|
actions?: any;
|
||||||
|
data?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITrigger {
|
||||||
|
run(startTime?: Date): Promise<IJSONValue>;
|
||||||
|
testRun(startTime?: Date): Promise<IJSONValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthentication {
|
export interface IAuthentication {
|
||||||
|
@@ -2,20 +2,16 @@ import * as React from 'react';
|
|||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete';
|
import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete';
|
||||||
|
import type { IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
|
||||||
interface ControlledAutocompleteProps extends AutocompleteProps<Option, boolean, boolean, boolean> {
|
interface ControlledAutocompleteProps extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
||||||
shouldUnregister?: boolean;
|
shouldUnregister?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option = {
|
const getOption = (options: readonly IFieldDropdownOption[], value: string) => options.find(option => option.value === value);
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOption = (options: readonly Option[], value: string) => options.find(option => option.value === value) || null;
|
|
||||||
|
|
||||||
function ControlledAutocomplete(props: ControlledAutocompleteProps): React.ReactElement {
|
function ControlledAutocomplete(props: ControlledAutocompleteProps): React.ReactElement {
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
@@ -48,8 +44,8 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
|||||||
options={options}
|
options={options}
|
||||||
value={getOption(options, field.value)}
|
value={getOption(options, field.value)}
|
||||||
onChange={(event, selectedOption, reason, details) => {
|
onChange={(event, selectedOption, reason, details) => {
|
||||||
const typedSelectedOption = selectedOption as Option;
|
const typedSelectedOption = selectedOption as IFieldDropdownOption;
|
||||||
if (typedSelectedOption?.value) {
|
if (Object.prototype.hasOwnProperty.call(typedSelectedOption, 'value')) {
|
||||||
controllerOnChange(typedSelectedOption.value);
|
controllerOnChange(typedSelectedOption.value);
|
||||||
} else {
|
} else {
|
||||||
controllerOnChange(typedSelectedOption);
|
controllerOnChange(typedSelectedOption);
|
||||||
|
@@ -57,7 +57,7 @@ function generateValidationSchema(substeps: ISubstep[]) {
|
|||||||
|
|
||||||
// base validation for the field if not exists
|
// base validation for the field if not exists
|
||||||
if (!substepArgumentValidations[key]) {
|
if (!substepArgumentValidations[key]) {
|
||||||
substepArgumentValidations[key] = yup.string();
|
substepArgumentValidations[key] = yup.mixed();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof substepArgumentValidations[key] === 'object') {
|
if (typeof substepArgumentValidations[key] === 'object') {
|
||||||
|
@@ -29,6 +29,9 @@ const validateSubstep = (substep: ISubstep, step: IStep) => {
|
|||||||
|
|
||||||
const argValue = step.parameters?.[arg.key];
|
const argValue = step.parameters?.[arg.key];
|
||||||
|
|
||||||
|
// `false` is an exceptional valid value
|
||||||
|
if (argValue === false) return true;
|
||||||
|
|
||||||
return Boolean(argValue);
|
return Boolean(argValue);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -52,7 +55,6 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
const formContext = useFormContext();
|
const formContext = useFormContext();
|
||||||
const [validationStatus, setValidationStatus] = React.useState<boolean | null>(validateSubstep(substep, formContext.getValues() as IStep));
|
const [validationStatus, setValidationStatus] = React.useState<boolean | null>(validateSubstep(substep, formContext.getValues() as IStep));
|
||||||
|
|
||||||
|
|
||||||
const handleChangeOnBlur = React.useCallback((key: string) => {
|
const handleChangeOnBlur = React.useCallback((key: string) => {
|
||||||
return (value: string) => {
|
return (value: string) => {
|
||||||
const currentValue = step.parameters?.[key];
|
const currentValue = step.parameters?.[key];
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useLazyQuery } from '@apollo/client';
|
import { useLazyQuery } from '@apollo/client';
|
||||||
import MuiTextField from '@mui/material/TextField';
|
import MuiTextField from '@mui/material/TextField';
|
||||||
import type { IField, IFieldDropdown, IJSONObject } from '@automatisch/types';
|
import type { IField, IFieldDropdown, IFieldDropdownOption, IJSONObject } from '@automatisch/types';
|
||||||
|
|
||||||
import useDynamicData from 'hooks/useDynamicData';
|
import useDynamicData from 'hooks/useDynamicData';
|
||||||
import { GET_DATA } from 'graphql/queries/get-data';
|
import { GET_DATA } from 'graphql/queries/get-data';
|
||||||
@@ -22,13 +22,9 @@ type RawOption = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Option = {
|
const computeArguments = (args: IFieldDropdown["source"]["arguments"]): IJSONObject => args.reduce((result, { name, value }) => ({ ...result, [name as string]: value }), {});
|
||||||
label: string;
|
const optionGenerator = (options: RawOption[]): IFieldDropdownOption[] => options?.map(({ name, value }) => ({ label: name as string, value: value }));
|
||||||
value: string;
|
const getOption = (options: IFieldDropdownOption[], value: string) => options?.find(option => option.value === value);
|
||||||
};
|
|
||||||
|
|
||||||
const optionGenerator = (options: RawOption[]): Option[] => options?.map(({ name, value }) => ({ label: name as string, value: value as string }));
|
|
||||||
const getOption = (options: Option[], value: string) => options?.find(option => option.value === value);
|
|
||||||
|
|
||||||
export default function InputCreator(props: InputCreatorProps): React.ReactElement {
|
export default function InputCreator(props: InputCreatorProps): React.ReactElement {
|
||||||
const {
|
const {
|
||||||
@@ -55,7 +51,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
if (type === 'dropdown') {
|
if (type === 'dropdown') {
|
||||||
const options = optionGenerator(data);
|
const preparedOptions = schema.options || optionGenerator(data?.getData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledAutocomplete
|
<ControlledAutocomplete
|
||||||
@@ -63,9 +59,9 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
fullWidth
|
fullWidth
|
||||||
disablePortal
|
disablePortal
|
||||||
disableClearable={required}
|
disableClearable={required}
|
||||||
options={options}
|
options={preparedOptions}
|
||||||
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
renderInput={(params) => <MuiTextField {...params} label={label} />}
|
||||||
value={getOption(options, value)}
|
value={getOption(preparedOptions, value)}
|
||||||
onChange={console.log}
|
onChange={console.log}
|
||||||
description={description}
|
description={description}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
@@ -8,6 +8,7 @@ export const UPDATE_STEP = gql`
|
|||||||
key
|
key
|
||||||
appKey
|
appKey
|
||||||
parameters
|
parameters
|
||||||
|
status
|
||||||
connection {
|
connection {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
@@ -64,6 +64,10 @@ export const GET_APPS = gql`
|
|||||||
description
|
description
|
||||||
variables
|
variables
|
||||||
dependsOn
|
dependsOn
|
||||||
|
options {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
source {
|
source {
|
||||||
type
|
type
|
||||||
name
|
name
|
||||||
|
Reference in New Issue
Block a user