Merge pull request #456 from automatisch/issue-455

feat: make flow editor read only when published
This commit is contained in:
Ömer Faruk Aydın
2022-08-24 22:24:44 +03:00
committed by GitHub
13 changed files with 87 additions and 25 deletions

View File

@@ -9,7 +9,7 @@ class Flow extends Base {
id!: string;
name!: string;
userId!: string;
active = false;
active: boolean;
steps?: [Step];
published_at: string;

View File

@@ -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) => <TextField {...params} label="Choose an app" />}
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) => <TextField {...params} label="Choose an event" />}
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
</Button>

View File

@@ -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) => <TextField {...params} label="Choose connection" />}
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
</Button>

View File

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

View File

@@ -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<HTMLInputElement>) => {
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 {
</Box>
<Box pr={1}>
<FormControlLabel
control={
<Switch checked={flow?.active ?? false} onChange={onFlowStatusUpdate} />
}
label={flow?.active ? formatMessage('flow.active') : formatMessage('flow.inactive')}
labelPlacement="start"
/>
<Button variant="contained" size="small" onClick={() => onFlowStatusUpdate(!flow.active)}>
{flow?.active ? formatMessage('flowEditor.unpublish') : formatMessage('flowEditor.publish')}
</Button>
</Box>
</Stack>
<Container maxWidth="md">
{!flow && !loading && 'not found'}
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !loading && 'not found'}
{flow && <Editor flow={flow} />}
{flow && <Editor flow={flow} />}
</EditorProvider>
</Container>
</Stack>
<Snackbar
open={!!flow?.active}
message={formatMessage('flowEditor.publishFlowCannotBeUpdated')}
anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
ContentProps={{ sx: { fontWeight: 300 }}}
action={(
<Button variant="contained" size="small" onClick={() => onFlowStatusUpdate(!flow.active)}>
{flow?.active ? formatMessage('flowEditor.unpublish') : formatMessage('flowEditor.publish')}
</Button>
)}
/>
</>
)
}

View File

@@ -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<HTMLButtonElement | null>(null);
const step: IStep = props.step;
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
@@ -204,7 +206,7 @@ export default function FlowStep(
<Box display="flex" flex={1} justifyContent="end">
{/* as there are no other actions besides "delete step", we hide the context menu. */}
{!isTrigger && (
{!isTrigger && !editorContext.readOnly && (
<IconButton
color="primary"
onClick={onContextMenuClick}

View File

@@ -5,6 +5,7 @@ import ListItem from '@mui/material/ListItem';
import Button from '@mui/material/Button';
import Stack from '@mui/material/Stack';
import { EditorContext } from 'contexts/Editor';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
import InputCreator from 'components/InputCreator';
import type { IField, IStep, ISubstep } from '@automatisch/types';
@@ -52,6 +53,7 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
arguments: args,
} = substep;
const editorContext = React.useContext(EditorContext);
const formContext = useFormContext();
const [validationStatus, setValidationStatus] = React.useState<boolean | null>(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}
/>
))}
</Stack>
@@ -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

View File

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

View File

@@ -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<HTMLDivElement | null>(null);
@@ -161,11 +163,11 @@ const PowerInput = (props: PowerInputProps) => {
<ClickAwayListener onClickAway={() => setSearch(null)}>
{/* ref-able single child for ClickAwayListener */}
<div style={{ width: '100%' }}>
<FakeInput>
<FakeInput disabled={disabled}>
<InputLabelWrapper>
<InputLabel
shrink={true}
// focused
disabled={disabled}
variant="outlined"
sx={{ bgcolor: 'white', display: 'inline-block', px: .75 }}
>
@@ -174,6 +176,7 @@ const PowerInput = (props: PowerInputProps) => {
</InputLabelWrapper>
<Editable
readOnly={disabled}
style={{ width: '100%' }}
renderElement={renderElement}
onKeyDown={onKeyDown}

View File

@@ -7,7 +7,7 @@ export const InputLabelWrapper = styled('div')`
left: -6px;
`;
export const FakeInput = styled('div')`
export const FakeInput = styled('div', { shouldForwardProp: prop => 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};
}

View File

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

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
interface IEditorContext {
readOnly: boolean;
}
export const EditorContext = React.createContext<IEditorContext>({ readOnly: false });
type EditorProviderProps = {
children: React.ReactNode;
value: IEditorContext;
}
export const EditorProvider = (props: EditorProviderProps): React.ReactElement => {
const { children, value } = props;
return (
<EditorContext.Provider
value={value}
>
{children}
</EditorContext.Provider>
);
};

View File

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