Merge pull request #889 from automatisch/feature/dynamic-fields
feat: add dynamic fields support
This commit is contained in:
@@ -51,25 +51,20 @@ export default defineAction({
|
|||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
additionalFields: {
|
||||||
{
|
type: 'query',
|
||||||
label: 'Bot name',
|
name: 'getDynamicFields',
|
||||||
key: 'botName',
|
arguments: [
|
||||||
type: 'string' as const,
|
{
|
||||||
required: true,
|
name: 'key',
|
||||||
value: 'Automatisch',
|
value: 'listFieldsAfterSendAsBot',
|
||||||
description:
|
},
|
||||||
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
|
{
|
||||||
variables: true,
|
name: 'parameters.sendAsBot',
|
||||||
},
|
value: '{parameters.sendAsBot}',
|
||||||
{
|
},
|
||||||
label: 'Bot icon',
|
],
|
||||||
key: 'botIcon',
|
},
|
||||||
type: 'string' as const,
|
|
||||||
required: false,
|
|
||||||
description:
|
|
||||||
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
|
|
||||||
variables: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
3
packages/backend/src/apps/slack/dynamic-fields/index.ts
Normal file
3
packages/backend/src/apps/slack/dynamic-fields/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import listFieldsAfterSendAsBot from './send-as-bot';
|
||||||
|
|
||||||
|
export default [listFieldsAfterSendAsBot];
|
@@ -0,0 +1,32 @@
|
|||||||
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'List fields after send as bot',
|
||||||
|
key: 'listFieldsAfterSendAsBot',
|
||||||
|
|
||||||
|
async run($: IGlobalVariable) {
|
||||||
|
if ($.step.parameters.sendAsBot) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Bot name',
|
||||||
|
key: 'botName',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: true,
|
||||||
|
value: 'Automatisch',
|
||||||
|
description:
|
||||||
|
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bot icon',
|
||||||
|
key: 'botIcon',
|
||||||
|
type: 'string' as const,
|
||||||
|
required: false,
|
||||||
|
description:
|
||||||
|
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
@@ -3,6 +3,7 @@ import addAuthHeader from './common/add-auth-header';
|
|||||||
import actions from './actions';
|
import actions from './actions';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import dynamicData from './dynamic-data';
|
import dynamicData from './dynamic-data';
|
||||||
|
import dynamicFields from './dynamic-fields';
|
||||||
|
|
||||||
export default defineApp({
|
export default defineApp({
|
||||||
name: 'Slack',
|
name: 'Slack',
|
||||||
@@ -17,4 +18,5 @@ export default defineApp({
|
|||||||
auth,
|
auth,
|
||||||
actions,
|
actions,
|
||||||
dynamicData,
|
dynamicData,
|
||||||
|
dynamicFields,
|
||||||
});
|
});
|
||||||
|
48
packages/backend/src/graphql/queries/get-dynamic-fields.ts
Normal file
48
packages/backend/src/graphql/queries/get-dynamic-fields.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { IDynamicFields, IJSONObject } from '@automatisch/types';
|
||||||
|
import Context from '../../types/express/context';
|
||||||
|
import App from '../../models/app';
|
||||||
|
import globalVariable from '../../helpers/global-variable';
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
stepId: string;
|
||||||
|
key: string;
|
||||||
|
parameters: IJSONObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDynamicFields = async (
|
||||||
|
_parent: unknown,
|
||||||
|
params: Params,
|
||||||
|
context: Context
|
||||||
|
) => {
|
||||||
|
const step = await context.currentUser
|
||||||
|
.$relatedQuery('steps')
|
||||||
|
.withGraphFetched({
|
||||||
|
connection: true,
|
||||||
|
flow: true,
|
||||||
|
})
|
||||||
|
.findById(params.stepId);
|
||||||
|
|
||||||
|
if (!step) return null;
|
||||||
|
|
||||||
|
const connection = step.connection;
|
||||||
|
|
||||||
|
if (!connection || !step.appKey) return null;
|
||||||
|
|
||||||
|
const app = await App.findOneByKey(step.appKey);
|
||||||
|
const $ = await globalVariable({ connection, app, flow: step.flow, step });
|
||||||
|
|
||||||
|
const command = app.dynamicFields.find(
|
||||||
|
(data: IDynamicFields) => data.key === params.key
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const parameterKey in params.parameters) {
|
||||||
|
const parameterValue = params.parameters[parameterKey];
|
||||||
|
$.step.parameters[parameterKey] = parameterValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const additionalFields = await command.run($) || [];
|
||||||
|
|
||||||
|
return additionalFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getDynamicFields;
|
@@ -9,6 +9,7 @@ import getExecution from './queries/get-execution';
|
|||||||
import getExecutions from './queries/get-executions';
|
import getExecutions from './queries/get-executions';
|
||||||
import getExecutionSteps from './queries/get-execution-steps';
|
import getExecutionSteps from './queries/get-execution-steps';
|
||||||
import getDynamicData from './queries/get-dynamic-data';
|
import getDynamicData from './queries/get-dynamic-data';
|
||||||
|
import getDynamicFields from './queries/get-dynamic-fields';
|
||||||
import getCurrentUser from './queries/get-current-user';
|
import getCurrentUser from './queries/get-current-user';
|
||||||
import getLicense from './queries/get-license.ee';
|
import getLicense from './queries/get-license.ee';
|
||||||
import healthcheck from './queries/healthcheck';
|
import healthcheck from './queries/healthcheck';
|
||||||
@@ -25,6 +26,7 @@ const queryResolvers = {
|
|||||||
getExecutions,
|
getExecutions,
|
||||||
getExecutionSteps,
|
getExecutionSteps,
|
||||||
getDynamicData,
|
getDynamicData,
|
||||||
|
getDynamicFields,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
getLicense,
|
getLicense,
|
||||||
healthcheck,
|
healthcheck,
|
||||||
|
@@ -28,6 +28,11 @@ type Query {
|
|||||||
key: String!
|
key: String!
|
||||||
parameters: JSONObject
|
parameters: JSONObject
|
||||||
): JSONObject
|
): JSONObject
|
||||||
|
getDynamicFields(
|
||||||
|
stepId: String!
|
||||||
|
key: String!
|
||||||
|
parameters: JSONObject
|
||||||
|
): [SubstepArgument]
|
||||||
getCurrentUser: User
|
getCurrentUser: User
|
||||||
getLicense: GetLicense
|
getLicense: GetLicense
|
||||||
healthcheck: AppHealth
|
healthcheck: AppHealth
|
||||||
@@ -65,38 +70,64 @@ directive @specifiedBy(
|
|||||||
url: String!
|
url: String!
|
||||||
) on SCALAR
|
) on SCALAR
|
||||||
|
|
||||||
|
type Trigger {
|
||||||
|
name: String
|
||||||
|
key: String
|
||||||
|
description: String
|
||||||
|
pollInterval: Int
|
||||||
|
type: String
|
||||||
|
substeps: [Substep]
|
||||||
|
}
|
||||||
|
|
||||||
type Action {
|
type Action {
|
||||||
name: String
|
name: String
|
||||||
key: String
|
key: String
|
||||||
description: String
|
description: String
|
||||||
substeps: [ActionSubstep]
|
substeps: [Substep]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstep {
|
type Substep {
|
||||||
key: String
|
key: String
|
||||||
name: String
|
name: String
|
||||||
arguments: [ActionSubstepArgument]
|
arguments: [SubstepArgument]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstepArgument {
|
type SubstepArgument {
|
||||||
label: String
|
label: String
|
||||||
key: String
|
key: String
|
||||||
type: String
|
type: String
|
||||||
description: String
|
description: String
|
||||||
required: Boolean
|
required: Boolean
|
||||||
variables: Boolean
|
variables: Boolean
|
||||||
options: [ArgumentOption]
|
options: [SubstepArgumentOption]
|
||||||
source: ActionSubstepArgumentSource
|
source: SubstepArgumentSource
|
||||||
|
additionalFields: SubstepArgumentAdditionalFields
|
||||||
dependsOn: [String]
|
dependsOn: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstepArgumentSource {
|
type SubstepArgumentOption {
|
||||||
type: String
|
label: String
|
||||||
name: String
|
value: JSONObject
|
||||||
arguments: [ActionSubstepArgumentSourceArgument]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionSubstepArgumentSourceArgument {
|
type SubstepArgumentSource {
|
||||||
|
type: String
|
||||||
|
name: String
|
||||||
|
arguments: [SubstepArgumentSourceArgument]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubstepArgumentSourceArgument {
|
||||||
|
name: String
|
||||||
|
value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubstepArgumentAdditionalFields {
|
||||||
|
type: String
|
||||||
|
name: String
|
||||||
|
arguments: [SubstepArgumentAdditionalFieldsArgument]
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubstepArgumentAdditionalFieldsArgument {
|
||||||
name: String
|
name: String
|
||||||
value: String
|
value: String
|
||||||
}
|
}
|
||||||
@@ -198,7 +229,7 @@ type Field {
|
|||||||
description: String
|
description: String
|
||||||
docUrl: String
|
docUrl: String
|
||||||
clickToCopy: Boolean
|
clickToCopy: Boolean
|
||||||
options: [ArgumentOption]
|
options: [SubstepArgumentOption]
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlowConnection {
|
type FlowConnection {
|
||||||
@@ -394,49 +425,6 @@ input StepInput {
|
|||||||
previousStep: PreviousStepInput
|
previousStep: PreviousStepInput
|
||||||
}
|
}
|
||||||
|
|
||||||
type Trigger {
|
|
||||||
name: String
|
|
||||||
key: String
|
|
||||||
description: String
|
|
||||||
pollInterval: Int
|
|
||||||
type: String
|
|
||||||
substeps: [TriggerSubstep]
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstep {
|
|
||||||
key: String
|
|
||||||
name: String
|
|
||||||
arguments: [TriggerSubstepArgument]
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstepArgument {
|
|
||||||
label: String
|
|
||||||
key: String
|
|
||||||
type: String
|
|
||||||
description: String
|
|
||||||
required: Boolean
|
|
||||||
variables: Boolean
|
|
||||||
source: TriggerSubstepArgumentSource
|
|
||||||
dependsOn: [String]
|
|
||||||
options: [ArgumentOption]
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstepArgumentSource {
|
|
||||||
type: String
|
|
||||||
name: String
|
|
||||||
arguments: [TriggerSubstepArgumentSourceArgument]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArgumentOption {
|
|
||||||
label: String
|
|
||||||
value: JSONObject
|
|
||||||
}
|
|
||||||
|
|
||||||
type TriggerSubstepArgumentSourceArgument {
|
|
||||||
name: String
|
|
||||||
value: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
id: String
|
id: String
|
||||||
email: String
|
email: String
|
||||||
|
@@ -149,6 +149,22 @@ class Step extends Base {
|
|||||||
|
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSetupFields() {
|
||||||
|
let setupSupsteps;
|
||||||
|
|
||||||
|
if (this.isTrigger) {
|
||||||
|
setupSupsteps = (await this.getTriggerCommand()).substeps;
|
||||||
|
} else {
|
||||||
|
setupSupsteps = (await this.getActionCommand()).substeps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingArguments = setupSupsteps.find(
|
||||||
|
(substep) => substep.key === 'chooseTrigger'
|
||||||
|
).arguments;
|
||||||
|
|
||||||
|
return existingArguments;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Step;
|
export default Step;
|
||||||
|
14
packages/types/index.d.ts
vendored
14
packages/types/index.d.ts
vendored
@@ -104,6 +104,7 @@ export interface IFieldDropdown {
|
|||||||
dependsOn?: string[];
|
dependsOn?: string[];
|
||||||
options?: IFieldDropdownOption[];
|
options?: IFieldDropdownOption[];
|
||||||
source?: IFieldDropdownSource;
|
source?: IFieldDropdownSource;
|
||||||
|
additionalFields?: IFieldDropdownAdditionalFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFieldDropdownSource {
|
export interface IFieldDropdownSource {
|
||||||
@@ -114,6 +115,14 @@ export interface IFieldDropdownSource {
|
|||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
export interface IFieldDropdownAdditionalFields {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
arguments: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IFieldDropdownOption {
|
export interface IFieldDropdownOption {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -167,6 +176,7 @@ export interface IApp {
|
|||||||
flowCount?: number;
|
flowCount?: number;
|
||||||
beforeRequest?: TBeforeRequest[];
|
beforeRequest?: TBeforeRequest[];
|
||||||
dynamicData?: IDynamicData;
|
dynamicData?: IDynamicData;
|
||||||
|
dynamicFields?: IDynamicFields;
|
||||||
triggers?: ITrigger[];
|
triggers?: ITrigger[];
|
||||||
actions?: IAction[];
|
actions?: IAction[];
|
||||||
connections?: IConnection[];
|
connections?: IConnection[];
|
||||||
@@ -180,6 +190,10 @@ export interface IDynamicData {
|
|||||||
[index: string]: any;
|
[index: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDynamicFields {
|
||||||
|
[index: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAuth {
|
export interface IAuth {
|
||||||
generateAuthUrl?($: IGlobalVariable): Promise<void>;
|
generateAuthUrl?($: IGlobalVariable): Promise<void>;
|
||||||
verifyCredentials?($: IGlobalVariable): Promise<void>;
|
verifyCredentials?($: IGlobalVariable): Promise<void>;
|
||||||
|
@@ -27,7 +27,7 @@ function ControlledAutocomplete(
|
|||||||
required = false,
|
required = false,
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
shouldUnregister,
|
shouldUnregister = true,
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
description,
|
description,
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import MuiTextField from '@mui/material/TextField';
|
import MuiTextField from '@mui/material/TextField';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import type { IField, IFieldDropdownOption } from '@automatisch/types';
|
import type { IField, IFieldDropdownOption } from '@automatisch/types';
|
||||||
|
|
||||||
|
import useDynamicFields from 'hooks/useDynamicFields';
|
||||||
import useDynamicData from 'hooks/useDynamicData';
|
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';
|
||||||
@@ -52,56 +54,111 @@ export default function InputCreator(
|
|||||||
} = schema;
|
} = schema;
|
||||||
|
|
||||||
const { data, loading } = useDynamicData(stepId, schema);
|
const { data, loading } = useDynamicData(stepId, schema);
|
||||||
|
const {
|
||||||
|
data: additionalFields,
|
||||||
|
loading: additionalFieldsLoading
|
||||||
|
} = useDynamicFields(stepId, schema);
|
||||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
if (type === 'dropdown') {
|
if (type === 'dropdown') {
|
||||||
const preparedOptions = schema.options || optionGenerator(data);
|
const preparedOptions = schema.options || optionGenerator(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlledAutocomplete
|
<React.Fragment>
|
||||||
name={computedName}
|
<ControlledAutocomplete
|
||||||
dependsOn={dependsOn}
|
name={computedName}
|
||||||
fullWidth
|
dependsOn={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}
|
||||||
/>
|
showOptionValue={showOptionValue}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||||
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{additionalFields?.map((field) => (
|
||||||
|
<InputCreator
|
||||||
|
key={field.key}
|
||||||
|
schema={field}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={disabled}
|
||||||
|
showOptionValue={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'string') {
|
if (type === 'string') {
|
||||||
if (variables) {
|
if (variables) {
|
||||||
return (
|
return (
|
||||||
<PowerInput
|
<React.Fragment>
|
||||||
label={label}
|
<PowerInput
|
||||||
description={description}
|
label={label}
|
||||||
name={computedName}
|
description={description}
|
||||||
required={required}
|
name={computedName}
|
||||||
disabled={disabled}
|
required={required}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||||
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{additionalFields?.map((field) => (
|
||||||
|
<InputCreator
|
||||||
|
key={field.key}
|
||||||
|
schema={field}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={disabled}
|
||||||
|
showOptionValue={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<React.Fragment>
|
||||||
defaultValue={value}
|
<TextField
|
||||||
required={required}
|
defaultValue={value}
|
||||||
placeholder=""
|
required={required}
|
||||||
readOnly={readOnly || disabled}
|
placeholder=""
|
||||||
onChange={onChange}
|
readOnly={readOnly || disabled}
|
||||||
onBlur={onBlur}
|
onChange={onChange}
|
||||||
name={computedName}
|
onBlur={onBlur}
|
||||||
label={label}
|
name={computedName}
|
||||||
fullWidth
|
label={label}
|
||||||
helperText={description}
|
fullWidth
|
||||||
clickToCopy={clickToCopy}
|
helperText={description}
|
||||||
/>
|
clickToCopy={clickToCopy}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(additionalFieldsLoading && !additionalFields?.length) && <div>
|
||||||
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{additionalFields?.map((field) => (
|
||||||
|
<InputCreator
|
||||||
|
key={field.key}
|
||||||
|
schema={field}
|
||||||
|
namePrefix="parameters"
|
||||||
|
stepId={stepId}
|
||||||
|
disabled={disabled}
|
||||||
|
showOptionValue={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -81,7 +81,7 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
name={name}
|
name={name}
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
shouldUnregister={false}
|
shouldUnregister={true}
|
||||||
render={({
|
render={({
|
||||||
field: {
|
field: {
|
||||||
value,
|
value,
|
||||||
|
@@ -38,9 +38,10 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
|||||||
required,
|
required,
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
shouldUnregister,
|
shouldUnregister = true,
|
||||||
clickToCopy,
|
clickToCopy = false,
|
||||||
readOnly,
|
readOnly = false,
|
||||||
|
disabled = false,
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
...textFieldProps
|
...textFieldProps
|
||||||
@@ -64,6 +65,7 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
|||||||
<MuiTextField
|
<MuiTextField
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
{...field}
|
{...field}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(...args) => {
|
onChange={(...args) => {
|
||||||
controllerOnChange(...args);
|
controllerOnChange(...args);
|
||||||
onChange?.(...args);
|
onChange?.(...args);
|
||||||
@@ -85,10 +87,3 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextField.defaultProps = {
|
|
||||||
readOnly: false,
|
|
||||||
disabled: false,
|
|
||||||
clickToCopy: false,
|
|
||||||
shouldUnregister: false,
|
|
||||||
};
|
|
||||||
|
@@ -122,6 +122,14 @@ export const GET_APPS = gql`
|
|||||||
value
|
value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
additionalFields {
|
||||||
|
type
|
||||||
|
name
|
||||||
|
arguments {
|
||||||
|
name
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
packages/web/src/graphql/queries/get-dynamic-fields.ts
Normal file
21
packages/web/src/graphql/queries/get-dynamic-fields.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_DYNAMIC_FIELDS = gql`
|
||||||
|
query GetDynamicFields(
|
||||||
|
$stepId: String!
|
||||||
|
$key: String!
|
||||||
|
$parameters: JSONObject
|
||||||
|
) {
|
||||||
|
getDynamicFields(stepId: $stepId, key: $key, parameters: $parameters) {
|
||||||
|
label
|
||||||
|
key
|
||||||
|
type
|
||||||
|
required
|
||||||
|
description
|
||||||
|
options {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
112
packages/web/src/hooks/useDynamicFields.ts
Normal file
112
packages/web/src/hooks/useDynamicFields.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useLazyQuery } from '@apollo/client';
|
||||||
|
import type { UseFormReturn } from 'react-hook-form';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import set from 'lodash/set';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import type {
|
||||||
|
IField,
|
||||||
|
IFieldDropdownAdditionalFields,
|
||||||
|
IJSONObject,
|
||||||
|
} from '@automatisch/types';
|
||||||
|
|
||||||
|
import { GET_DYNAMIC_FIELDS } from 'graphql/queries/get-dynamic-fields';
|
||||||
|
|
||||||
|
const variableRegExp = /({.*?})/g;
|
||||||
|
|
||||||
|
// TODO: extract this function to a separate file
|
||||||
|
function computeArguments(
|
||||||
|
args: IFieldDropdownAdditionalFields['arguments'],
|
||||||
|
getValues: UseFormReturn['getValues']
|
||||||
|
): IJSONObject {
|
||||||
|
const initialValue = {};
|
||||||
|
return args.reduce((result, { name, value }) => {
|
||||||
|
const isVariable = variableRegExp.test(value);
|
||||||
|
|
||||||
|
if (isVariable) {
|
||||||
|
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||||
|
const computedValue = getValues(sanitizedFieldPath);
|
||||||
|
|
||||||
|
if (computedValue === undefined)
|
||||||
|
throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||||
|
|
||||||
|
set(result, name, computedValue);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(result, name, value);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the dynamic fields for the given step.
|
||||||
|
* This hook must be within a react-hook-form context.
|
||||||
|
*
|
||||||
|
* @param stepId - the id of the step
|
||||||
|
* @param schema - the field schema that needs the dynamic fields
|
||||||
|
*/
|
||||||
|
function useDynamicFields(stepId: string | undefined, schema: IField) {
|
||||||
|
const lastComputedVariables = React.useRef({});
|
||||||
|
const [getDynamicFields, { called, data, loading }] =
|
||||||
|
useLazyQuery(GET_DYNAMIC_FIELDS);
|
||||||
|
const { getValues } = useFormContext();
|
||||||
|
const formValues = getValues();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return `null` when even a field is missing value.
|
||||||
|
*
|
||||||
|
* This must return the same reference if no computed variable is changed.
|
||||||
|
* Otherwise, it causes redundant network request!
|
||||||
|
*/
|
||||||
|
const computedVariables = React.useMemo(() => {
|
||||||
|
if (schema.type === 'dropdown' && schema.additionalFields) {
|
||||||
|
try {
|
||||||
|
const variables = computeArguments(schema.additionalFields.arguments, getValues);
|
||||||
|
|
||||||
|
// if computed variables are the same, return the last computed variables.
|
||||||
|
if (isEqual(variables, lastComputedVariables.current)) {
|
||||||
|
return lastComputedVariables.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastComputedVariables.current = variables;
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
/**
|
||||||
|
* `formValues` is to trigger recomputation when form is updated.
|
||||||
|
* `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`.
|
||||||
|
*/
|
||||||
|
}, [schema, formValues, getValues]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (
|
||||||
|
schema.type === 'dropdown' &&
|
||||||
|
stepId &&
|
||||||
|
schema.additionalFields &&
|
||||||
|
computedVariables
|
||||||
|
) {
|
||||||
|
getDynamicFields({
|
||||||
|
variables: {
|
||||||
|
stepId,
|
||||||
|
...computedVariables,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [getDynamicFields, stepId, schema, computedVariables]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
called,
|
||||||
|
data: data?.getDynamicFields as IField[] | undefined,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDynamicFields;
|
Reference in New Issue
Block a user