diff --git a/packages/backend/src/models/flow.ts b/packages/backend/src/models/flow.ts index 3d6a82d4..46169e8e 100644 --- a/packages/backend/src/models/flow.ts +++ b/packages/backend/src/models/flow.ts @@ -9,7 +9,7 @@ class Flow extends Base { id!: string; name!: string; userId!: string; - active = false; + active: boolean; steps?: [Step]; published_at: string; diff --git a/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx b/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx index bc61c3c2..17470b1b 100644 --- a/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx +++ b/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx @@ -8,6 +8,7 @@ import ListItem from '@mui/material/ListItem'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; +import { EditorContext } from 'contexts/Editor'; import { GET_APPS } from 'graphql/queries/get-apps'; import FlowSubstepTitle from 'components/FlowSubstepTitle'; import type { IApp, IStep, ISubstep } from '@automatisch/types'; @@ -40,6 +41,8 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R onChange, } = props; + const editorContext = React.useContext(EditorContext); + const isTrigger = step.type === 'trigger'; const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }}); @@ -111,6 +114,7 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R fullWidth disablePortal disableClearable + disabled={editorContext.readOnly} options={appOptions} renderInput={(params) => } value={getOption(appOptions, step.appKey)} @@ -127,6 +131,7 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R fullWidth disablePortal disableClearable + disabled={editorContext.readOnly} options={actionOptions} renderInput={(params) => } value={getOption(actionOptions, step.key)} @@ -140,7 +145,7 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R variant="contained" onClick={onSubmit} sx={{ mt: 2 }} - disabled={!valid} + disabled={!valid || editorContext.readOnly} > Continue diff --git a/packages/web/src/components/ChooseConnectionSubstep/index.tsx b/packages/web/src/components/ChooseConnectionSubstep/index.tsx index 10aa73e4..9fdeaec8 100644 --- a/packages/web/src/components/ChooseConnectionSubstep/index.tsx +++ b/packages/web/src/components/ChooseConnectionSubstep/index.tsx @@ -6,6 +6,7 @@ import Collapse from '@mui/material/Collapse'; import ListItem from '@mui/material/ListItem'; import Autocomplete from '@mui/material/Autocomplete'; +import { EditorContext } from 'contexts/Editor'; import FlowSubstepTitle from 'components/FlowSubstepTitle'; import type { IApp, IConnection, IStep, ISubstep } from '@automatisch/types'; import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections'; @@ -42,6 +43,7 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea connection, appKey, } = step; + const editorContext = React.useContext(EditorContext); const { data, loading } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey }}); // TODO: show detailed error when connection test/verification fails const [ @@ -118,6 +120,7 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea fullWidth disablePortal disableClearable + disabled={editorContext.readOnly} options={connectionOptions} renderInput={(params) => } value={getOption(connectionOptions, connection?.id)} @@ -130,7 +133,7 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea variant="contained" onClick={onSubmit} sx={{ mt: 2 }} - disabled={testResultLoading || !connection?.verified} + disabled={testResultLoading || !connection?.verified || editorContext.readOnly} > Continue diff --git a/packages/web/src/components/Editor/index.tsx b/packages/web/src/components/Editor/index.tsx index 2b6c242f..16a77d91 100644 --- a/packages/web/src/components/Editor/index.tsx +++ b/packages/web/src/components/Editor/index.tsx @@ -3,12 +3,12 @@ import { useMutation } from '@apollo/client'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import AddIcon from '@mui/icons-material/Add'; +import type { IFlow } from '@automatisch/types'; import { GET_FLOW } from 'graphql/queries/get-flow'; import { CREATE_STEP } from 'graphql/mutations/create-step'; import { UPDATE_STEP } from 'graphql/mutations/update-step'; import FlowStep from 'components/FlowStep'; -import type { IFlow } from '@automatisch/types'; type EditorProps = { flow: IFlow; diff --git a/packages/web/src/components/EditorLayout/index.tsx b/packages/web/src/components/EditorLayout/index.tsx index 84f8df26..a7bbbf23 100644 --- a/packages/web/src/components/EditorLayout/index.tsx +++ b/packages/web/src/components/EditorLayout/index.tsx @@ -3,12 +3,13 @@ import { Link, useParams } from 'react-router-dom'; import { useMutation, useQuery } from '@apollo/client'; import Stack from '@mui/material/Stack'; import Box from '@mui/material/Box'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Switch from '@mui/material/Switch'; +import Button from '@mui/material/Button'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import Snackbar from '@mui/material/Snackbar'; +import { EditorProvider } from 'contexts/Editor'; import EditableTypography from 'components/EditableTypography'; import Container from 'components/Container'; import Editor from 'components/Editor'; @@ -45,9 +46,7 @@ export default function EditorLayout(): React.ReactElement { }); }, [flow?.id]); - const onFlowStatusUpdate = React.useCallback(async (event: React.ChangeEvent) => { - const active = event.target.checked; - + const onFlowStatusUpdate = React.useCallback(async (active: boolean) => { await updateFlowStatus({ variables: { input: { @@ -93,22 +92,32 @@ export default function EditorLayout(): React.ReactElement { - - } - label={flow?.active ? formatMessage('flow.active') : formatMessage('flow.inactive')} - labelPlacement="start" - /> + - {!flow && !loading && 'not found'} + + {!flow && !loading && 'not found'} - {flow && } + {flow && } + + + onFlowStatusUpdate(!flow.active)}> + {flow?.active ? formatMessage('flowEditor.unpublish') : formatMessage('flowEditor.publish')} + + )} + /> ) } diff --git a/packages/web/src/components/FlowStep/index.tsx b/packages/web/src/components/FlowStep/index.tsx index d41e2fc3..6418b2f2 100644 --- a/packages/web/src/components/FlowStep/index.tsx +++ b/packages/web/src/components/FlowStep/index.tsx @@ -15,6 +15,7 @@ import * as yup from 'yup'; import type { BaseSchema } from 'yup'; import type { IApp, IField, IStep, ISubstep } from '@automatisch/types'; +import { EditorContext } from 'contexts/Editor'; import { StepExecutionsProvider } from 'contexts/StepExecutions'; import TestSubstep from 'components/TestSubstep'; import FlowSubstep from 'components/FlowSubstep'; @@ -99,6 +100,7 @@ export default function FlowStep( props: FlowStepProps ): React.ReactElement | null { const { collapsed, onChange } = props; + const editorContext = React.useContext(EditorContext); const contextButtonRef = React.useRef(null); const step: IStep = props.step; const [anchorEl, setAnchorEl] = React.useState( @@ -204,7 +206,7 @@ export default function FlowStep( {/* as there are no other actions besides "delete step", we hide the context menu. */} - {!isTrigger && ( + {!isTrigger && !editorContext.readOnly && ( (validateSubstep(substep, formContext.getValues() as IStep)); @@ -105,6 +107,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement { schema={argument} namePrefix="parameters" stepId={step.id} + disabled={editorContext.readOnly} /> ))} @@ -114,7 +117,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement { variant="contained" onClick={onSubmit} sx={{ mt: 2 }} - disabled={!validationStatus} + disabled={!validationStatus || editorContext.readOnly} type="submit" > Continue diff --git a/packages/web/src/components/InputCreator/index.tsx b/packages/web/src/components/InputCreator/index.tsx index f1b74fd1..6c1469fc 100644 --- a/packages/web/src/components/InputCreator/index.tsx +++ b/packages/web/src/components/InputCreator/index.tsx @@ -15,6 +15,7 @@ type InputCreatorProps = { schema: IField; namePrefix?: string; stepId?: string; + disabled?: boolean; }; type RawOption = { @@ -33,6 +34,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme schema, namePrefix, stepId, + disabled, } = props; const { @@ -65,6 +67,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme onChange={console.log} description={description} loading={loading} + disabled={disabled} /> ); } @@ -77,6 +80,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme description={description} name={computedName} required={required} + disabled={disabled} /> ); } @@ -86,8 +90,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme defaultValue={value} required={required} placeholder="" - disabled={readOnly} - readOnly={readOnly} + readOnly={readOnly || disabled} onChange={onChange} onBlur={onBlur} name={computedName} diff --git a/packages/web/src/components/PowerInput/index.tsx b/packages/web/src/components/PowerInput/index.tsx index 47a0b368..06eb520b 100644 --- a/packages/web/src/components/PowerInput/index.tsx +++ b/packages/web/src/components/PowerInput/index.tsx @@ -38,6 +38,7 @@ type PowerInputProps = { description?: string; docUrl?: string; clickToCopy?: boolean; + disabled?: boolean; } const PowerInput = (props: PowerInputProps) => { @@ -49,6 +50,7 @@ const PowerInput = (props: PowerInputProps) => { label, required, description, + disabled, } = props; const priorStepsWithExecutions = React.useContext(StepExecutionsContext); const editorRef = React.useRef(null); @@ -161,11 +163,11 @@ const PowerInput = (props: PowerInputProps) => { setSearch(null)}> {/* ref-able single child for ClickAwayListener */}
- + @@ -174,6 +176,7 @@ const PowerInput = (props: PowerInputProps) => { prop !== 'disabled'})<{ disabled?: boolean }>` border: 1px solid #eee; min-height: 52px; width: 100%; @@ -17,6 +17,11 @@ export const FakeInput = styled('div')` border-color: rgba(0, 0, 0, 0.23); position: relative; + ${({ disabled, theme }) => !!disabled && ` + color: ${theme.palette.action.disabled}, + border-color: ${theme.palette.action.disabled}, + `} + &:hover { border-color: ${({ theme }) => theme.palette.text.primary}; } diff --git a/packages/web/src/components/TestSubstep/index.tsx b/packages/web/src/components/TestSubstep/index.tsx index 64f99dc8..4673120a 100644 --- a/packages/web/src/components/TestSubstep/index.tsx +++ b/packages/web/src/components/TestSubstep/index.tsx @@ -6,6 +6,7 @@ import ListItem from '@mui/material/ListItem'; import Alert from '@mui/material/Alert'; import LoadingButton from '@mui/lab/LoadingButton'; +import { EditorContext } from 'contexts/Editor'; import JSONViewer from 'components/JSONViewer'; import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow'; import FlowSubstepTitle from 'components/FlowSubstepTitle'; @@ -31,6 +32,7 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement { step, } = props; + const editorContext = React.useContext(EditorContext); const [executeFlow, { data, error, loading }] = useMutation(EXECUTE_FLOW, { context: { autoSnackbar: false }}); const response = data?.executeFlow?.data; @@ -74,6 +76,7 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement { onClick={handleSubmit} sx={{ mt: 2 }} loading={loading} + disabled={editorContext.readOnly} color="primary" > Test & Continue diff --git a/packages/web/src/contexts/Editor.tsx b/packages/web/src/contexts/Editor.tsx new file mode 100644 index 00000000..02968957 --- /dev/null +++ b/packages/web/src/contexts/Editor.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; + +interface IEditorContext { + readOnly: boolean; +} + +export const EditorContext = React.createContext({ readOnly: false }); + +type EditorProviderProps = { + children: React.ReactNode; + value: IEditorContext; +} + +export const EditorProvider = (props: EditorProviderProps): React.ReactElement => { + const { children, value } = props; + return ( + + {children} + + ); +}; diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index dea54604..6e7ed750 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -40,6 +40,9 @@ "createFlow.creating": "Creating a flow...", "flow.active": "ON", "flow.inactive": "OFF", + "flowEditor.publish": "PUBLISH", + "flowEditor.unpublish": "UNPUBLISH", + "flowEditor.publishFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.", "flow.createdAt": "created {datetime}", "flow.updatedAt": "updated {datetime}", "flow.view": "View",