style: auto format whole project
This commit is contained in:
@@ -25,9 +25,12 @@
|
||||
-->
|
||||
<title>Automatisch</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
@@ -12,21 +12,18 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
||||
type AccountDropdownMenuProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
anchorEl: MenuProps["anchorEl"];
|
||||
anchorEl: MenuProps['anchorEl'];
|
||||
id: string;
|
||||
}
|
||||
};
|
||||
|
||||
function AccountDropdownMenu(props: AccountDropdownMenuProps): React.ReactElement {
|
||||
function AccountDropdownMenu(
|
||||
props: AccountDropdownMenuProps
|
||||
): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const authentication = useAuthentication();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
open,
|
||||
onClose,
|
||||
anchorEl,
|
||||
id
|
||||
} = props
|
||||
const { open, onClose, anchorEl, id } = props;
|
||||
|
||||
const logout = async () => {
|
||||
authentication.updateToken('');
|
||||
@@ -53,17 +50,11 @@ function AccountDropdownMenu(props: AccountDropdownMenuProps): React.ReactElemen
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={URLS.SETTINGS_DASHBOARD}
|
||||
>
|
||||
<MenuItem component={Link} to={URLS.SETTINGS_DASHBOARD}>
|
||||
{formatMessage('accountDropdownMenu.settings')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
data-test="logout-item"
|
||||
>
|
||||
<MenuItem onClick={logout} data-test="logout-item">
|
||||
{formatMessage('accountDropdownMenu.logout')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
@@ -14,9 +14,12 @@ import InputCreator from 'components/InputCreator';
|
||||
import type { IApp, IField } from '@automatisch/types';
|
||||
import { Form } from './style';
|
||||
|
||||
const generateDocsLink = (link: string) => (str: string) => (
|
||||
<a href={link} target="_blank">{str}</a>
|
||||
);
|
||||
const generateDocsLink = (link: string) => (str: string) =>
|
||||
(
|
||||
<a href={link} target="_blank">
|
||||
{str}
|
||||
</a>
|
||||
);
|
||||
|
||||
type AddAppConnectionProps = {
|
||||
onClose: (response: Record<string, unknown>) => void;
|
||||
@@ -26,79 +29,90 @@ type AddAppConnectionProps = {
|
||||
|
||||
type Response = {
|
||||
[key: string]: any;
|
||||
}
|
||||
};
|
||||
|
||||
export default function AddAppConnection(props: AddAppConnectionProps): React.ReactElement {
|
||||
export default function AddAppConnection(
|
||||
props: AddAppConnectionProps
|
||||
): React.ReactElement {
|
||||
const { application, connectionId, onClose } = props;
|
||||
const { name, authDocUrl, key, auth } = application;
|
||||
const formatMessage = useFormatMessage();
|
||||
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
|
||||
const [inProgress, setInProgress] = React.useState(false);
|
||||
const hasConnection = Boolean(connectionId);
|
||||
const steps = hasConnection ? auth?.reconnectionSteps : auth?.authenticationSteps;
|
||||
const steps = hasConnection
|
||||
? auth?.reconnectionSteps
|
||||
: auth?.authenticationSteps;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ source: 'automatisch', payload: window.location.search });
|
||||
window.opener.postMessage({
|
||||
source: 'automatisch',
|
||||
payload: window.location.search,
|
||||
});
|
||||
window.close();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(async (data) => {
|
||||
if (!steps) return;
|
||||
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
|
||||
async (data) => {
|
||||
if (!steps) return;
|
||||
|
||||
setInProgress(true);
|
||||
setErrorMessage(null);
|
||||
setInProgress(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
const response: Response = {
|
||||
key,
|
||||
connection: {
|
||||
id: connectionId
|
||||
},
|
||||
fields: data,
|
||||
};
|
||||
const response: Response = {
|
||||
key,
|
||||
connection: {
|
||||
id: connectionId,
|
||||
},
|
||||
fields: data,
|
||||
};
|
||||
|
||||
let stepIndex = 0;
|
||||
while (stepIndex < steps.length) {
|
||||
const step = steps[stepIndex];
|
||||
const variables = computeAuthStepVariables(step.arguments, response);
|
||||
let stepIndex = 0;
|
||||
while (stepIndex < steps.length) {
|
||||
const step = steps[stepIndex];
|
||||
const variables = computeAuthStepVariables(step.arguments, response);
|
||||
|
||||
try {
|
||||
const stepResponse = await processStep(step, variables);
|
||||
try {
|
||||
const stepResponse = await processStep(step, variables);
|
||||
|
||||
response[step.name] = stepResponse;
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
console.log(error);
|
||||
setErrorMessage(error.message);
|
||||
setInProgress(false);
|
||||
response[step.name] = stepResponse;
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
console.log(error);
|
||||
setErrorMessage(error.message);
|
||||
setInProgress(false);
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
stepIndex++;
|
||||
|
||||
if (stepIndex === steps.length) {
|
||||
onClose(response);
|
||||
}
|
||||
}
|
||||
|
||||
stepIndex++;
|
||||
|
||||
if (stepIndex === steps.length) {
|
||||
onClose(response);
|
||||
}
|
||||
}
|
||||
|
||||
setInProgress(false);
|
||||
}, [connectionId, key, steps, onClose]);
|
||||
setInProgress(false);
|
||||
},
|
||||
[connectionId, key, steps, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={onClose} data-test="add-app-connection-dialog">
|
||||
<DialogTitle>{hasConnection ? formatMessage('app.reconnectConnection') : formatMessage('app.addConnection')}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{hasConnection
|
||||
? formatMessage('app.reconnectConnection')
|
||||
: formatMessage('app.addConnection')}
|
||||
</DialogTitle>
|
||||
|
||||
{authDocUrl && (
|
||||
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
||||
{formatMessage(
|
||||
'addAppConnection.callToDocs',
|
||||
{
|
||||
appName: name,
|
||||
docsLink: generateDocsLink(authDocUrl)
|
||||
}
|
||||
)}
|
||||
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
||||
{formatMessage('addAppConnection.callToDocs', {
|
||||
appName: name,
|
||||
docsLink: generateDocsLink(authDocUrl),
|
||||
})}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
@@ -111,7 +125,9 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
|
||||
<DialogContent>
|
||||
<DialogContentText tabIndex={-1} component="div">
|
||||
<Form onSubmit={submitHandler}>
|
||||
{auth?.fields?.map((field: IField) => (<InputCreator key={field.key} schema={field} />))}
|
||||
{auth?.fields?.map((field: IField) => (
|
||||
<InputCreator key={field.key} schema={field} />
|
||||
))}
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
@@ -128,4 +144,4 @@ export default function AddAppConnection(props: AddAppConnectionProps): React.Re
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -13,9 +13,12 @@ const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const authentication = useAuthentication();
|
||||
|
||||
const onError = React.useCallback((message) => {
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
}, [enqueueSnackbar]);
|
||||
const onError = React.useCallback(
|
||||
(message) => {
|
||||
enqueueSnackbar(message, { variant: 'error' });
|
||||
},
|
||||
[enqueueSnackbar]
|
||||
);
|
||||
|
||||
const client = React.useMemo(() => {
|
||||
return mutateAndGetClient({
|
||||
@@ -24,9 +27,7 @@ const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
||||
});
|
||||
}, [onError, authentication]);
|
||||
|
||||
return (
|
||||
<BaseApolloProvider client={client} {...props} />
|
||||
);
|
||||
return <BaseApolloProvider client={client} {...props} />;
|
||||
};
|
||||
|
||||
export default ApolloProvider;
|
||||
|
@@ -20,23 +20,21 @@ type AppBarProps = {
|
||||
drawerOpen: boolean;
|
||||
onDrawerOpen: () => void;
|
||||
onDrawerClose: () => void;
|
||||
maxWidth?: ContainerProps["maxWidth"];
|
||||
maxWidth?: ContainerProps['maxWidth'];
|
||||
};
|
||||
|
||||
const accountMenuId = 'account-menu';
|
||||
|
||||
export default function AppBar(props: AppBarProps): React.ReactElement {
|
||||
const {
|
||||
drawerOpen,
|
||||
onDrawerOpen,
|
||||
onDrawerClose,
|
||||
maxWidth = false,
|
||||
} = props;
|
||||
const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
||||
noSsr: true,
|
||||
});
|
||||
|
||||
const [accountMenuAnchorElement, setAccountMenuAnchorElement] = React.useState<null | HTMLElement>(null);
|
||||
const [accountMenuAnchorElement, setAccountMenuAnchorElement] =
|
||||
React.useState<null | HTMLElement>(null);
|
||||
|
||||
const isMenuOpen = Boolean(accountMenuAnchorElement);
|
||||
|
||||
@@ -65,11 +63,7 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
|
||||
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<Link to={URLS.DASHBOARD}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
noWrap
|
||||
>
|
||||
<Typography variant="h6" component="h1" noWrap>
|
||||
<FormattedMessage id="brandText" />
|
||||
</Typography>
|
||||
</Link>
|
||||
|
@@ -19,17 +19,22 @@ type ContextMenuProps = {
|
||||
anchorEl: PopoverProps['anchorEl'];
|
||||
};
|
||||
|
||||
export default function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
||||
export default function ContextMenu(
|
||||
props: ContextMenuProps
|
||||
): React.ReactElement {
|
||||
const { appKey, connectionId, onClose, onMenuItemClick, anchorEl } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const createActionHandler = React.useCallback((action: Action) => {
|
||||
return function clickHandler(event: React.MouseEvent) {
|
||||
onMenuItemClick(event, action);
|
||||
const createActionHandler = React.useCallback(
|
||||
(action: Action) => {
|
||||
return function clickHandler(event: React.MouseEvent) {
|
||||
onMenuItemClick(event, action);
|
||||
|
||||
onClose();
|
||||
};
|
||||
}, [onMenuItemClick, onClose]);
|
||||
onClose();
|
||||
};
|
||||
},
|
||||
[onMenuItemClick, onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@@ -63,4 +68,4 @@ export default function ContextMenu(props: ContextMenuProps): React.ReactElement
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -20,13 +20,11 @@ import { CardContent, Typography } from './style';
|
||||
|
||||
type AppConnectionRowProps = {
|
||||
connection: IConnection;
|
||||
}
|
||||
};
|
||||
|
||||
const countTranslation = (value: React.ReactNode) => (
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
{value}
|
||||
</Typography>
|
||||
<Typography variant="body1">{value}</Typography>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
@@ -34,15 +32,21 @@ const countTranslation = (value: React.ReactNode) => (
|
||||
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [verificationVisible, setVerificationVisible] = React.useState(false);
|
||||
const [testConnection, { called: testCalled, loading: testLoading }] = useLazyQuery(TEST_CONNECTION, {
|
||||
fetchPolicy: 'network-only',
|
||||
onCompleted: () => { setTimeout(() => setVerificationVisible(false), 3000); },
|
||||
onError: () => { setTimeout(() => setVerificationVisible(false), 3000); },
|
||||
});
|
||||
const [testConnection, { called: testCalled, loading: testLoading }] =
|
||||
useLazyQuery(TEST_CONNECTION, {
|
||||
fetchPolicy: 'network-only',
|
||||
onCompleted: () => {
|
||||
setTimeout(() => setVerificationVisible(false), 3000);
|
||||
},
|
||||
onError: () => {
|
||||
setTimeout(() => setVerificationVisible(false), 3000);
|
||||
},
|
||||
});
|
||||
const [deleteConnection] = useMutation(DELETE_CONNECTION);
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const { id, key, formattedData, verified, createdAt, flowCount } = props.connection;
|
||||
const { id, key, formattedData, verified, createdAt, flowCount } =
|
||||
props.connection;
|
||||
|
||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
|
||||
@@ -52,47 +56,52 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
};
|
||||
|
||||
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
|
||||
const onContextMenuAction = React.useCallback(async (event, action: { [key: string]: string }) => {
|
||||
if (action.type === 'delete') {
|
||||
await deleteConnection({
|
||||
variables: { input: { id } },
|
||||
update: (cache) => {
|
||||
const connectionCacheId = cache.identify({
|
||||
__typename: 'Connection',
|
||||
id,
|
||||
});
|
||||
const onContextMenuAction = React.useCallback(
|
||||
async (event, action: { [key: string]: string }) => {
|
||||
if (action.type === 'delete') {
|
||||
await deleteConnection({
|
||||
variables: { input: { id } },
|
||||
update: (cache) => {
|
||||
const connectionCacheId = cache.identify({
|
||||
__typename: 'Connection',
|
||||
id,
|
||||
});
|
||||
|
||||
cache.evict({
|
||||
id: connectionCacheId,
|
||||
});
|
||||
}
|
||||
});
|
||||
cache.evict({
|
||||
id: connectionCacheId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('connection.deletedMessage'), { variant: 'success' });
|
||||
} else if (action.type === 'test') {
|
||||
setVerificationVisible(true);
|
||||
testConnection({ variables: { id } });
|
||||
}
|
||||
}, [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]);
|
||||
enqueueSnackbar(formatMessage('connection.deletedMessage'), {
|
||||
variant: 'success',
|
||||
});
|
||||
} else if (action.type === 'test') {
|
||||
setVerificationVisible(true);
|
||||
testConnection({ variables: { id } });
|
||||
}
|
||||
},
|
||||
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
|
||||
);
|
||||
|
||||
const relativeCreatedAt = DateTime.fromMillis(parseInt(createdAt, 10)).toRelative();
|
||||
const relativeCreatedAt = DateTime.fromMillis(
|
||||
parseInt(createdAt, 10)
|
||||
).toRelative();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card sx={{ my: 2 }} data-test="app-connection-row">
|
||||
<CardActionArea onClick={onContextMenuClick}>
|
||||
<CardContent>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="flex-start"
|
||||
spacing={1}
|
||||
>
|
||||
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
|
||||
<Typography variant="h6" sx={{ textAlign: 'left' }}>
|
||||
{formattedData?.screenName}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption">
|
||||
{formatMessage('connection.addedAt', { datetime: relativeCreatedAt })}
|
||||
{formatMessage('connection.addedAt', {
|
||||
datetime: relativeCreatedAt,
|
||||
})}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -101,27 +110,42 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
{verificationVisible && testCalled && testLoading && (
|
||||
<>
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="caption">{formatMessage('connection.testing')}</Typography>
|
||||
<Typography variant="caption">
|
||||
{formatMessage('connection.testing')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible && testCalled && !testLoading && verified && (
|
||||
<>
|
||||
<CheckCircleIcon fontSize="small" color="success" />
|
||||
<Typography variant="caption">{formatMessage('connection.testSuccessful')}</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible && testCalled && !testLoading && !verified && (
|
||||
<>
|
||||
<ErrorIcon fontSize="small" color="error" />
|
||||
<Typography variant="caption">{formatMessage('connection.testFailed')}</Typography>
|
||||
<Typography variant="caption">
|
||||
{formatMessage('connection.testSuccessful')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{verificationVisible &&
|
||||
testCalled &&
|
||||
!testLoading &&
|
||||
!verified && (
|
||||
<>
|
||||
<ErrorIcon fontSize="small" color="error" />
|
||||
<Typography variant="caption">
|
||||
{formatMessage('connection.testFailed')}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
|
||||
{formatMessage('connection.flowCount', { count: countTranslation(flowCount) })}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
sx={{ display: ['none', 'inline-block'] }}
|
||||
>
|
||||
{formatMessage('connection.flowCount', {
|
||||
count: countTranslation(flowCount),
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -132,13 +156,15 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
|
||||
{anchorEl && <ConnectionContextMenu
|
||||
appKey={key}
|
||||
connectionId={id}
|
||||
onClose={handleClose}
|
||||
onMenuItemClick={onContextMenuAction}
|
||||
anchorEl={anchorEl}
|
||||
/>}
|
||||
{anchorEl && (
|
||||
<ConnectionContextMenu
|
||||
appKey={key}
|
||||
connectionId={id}
|
||||
onClose={handleClose}
|
||||
onMenuItemClick={onContextMenuAction}
|
||||
anchorEl={anchorEl}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
|
||||
export const Typography = styled(MuiTypography)(() => ({
|
||||
textAlign: 'center',
|
||||
display: 'inline-block',
|
||||
|
@@ -10,12 +10,16 @@ import * as URLS from 'config/urls';
|
||||
|
||||
type AppConnectionsProps = {
|
||||
appKey: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function AppConnections(props: AppConnectionsProps): React.ReactElement {
|
||||
export default function AppConnections(
|
||||
props: AppConnectionsProps
|
||||
): React.ReactElement {
|
||||
const { appKey } = props;
|
||||
const formatMessage = useFormatMessage();
|
||||
const { data } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey } });
|
||||
const { data } = useQuery(GET_APP_CONNECTIONS, {
|
||||
variables: { key: appKey },
|
||||
});
|
||||
const appConnections: IConnection[] = data?.getApp?.connections || [];
|
||||
|
||||
const hasConnections = appConnections?.length;
|
||||
@@ -35,5 +39,5 @@ export default function AppConnections(props: AppConnectionsProps): React.ReactE
|
||||
<AppConnectionRow key={appConnection.id} connection={appConnection} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
};
|
||||
);
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import type { IFlow } from '@automatisch/types';
|
||||
|
||||
type AppFlowsProps = {
|
||||
appKey: string;
|
||||
}
|
||||
};
|
||||
|
||||
const FLOW_PER_PAGE = 10;
|
||||
|
||||
@@ -27,11 +27,13 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const connectionId = searchParams.get('connectionId') || undefined;
|
||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||
const { data } = useQuery(GET_FLOWS, { variables: {
|
||||
appKey,
|
||||
connectionId,
|
||||
...getLimitAndOffset(page)
|
||||
}});
|
||||
const { data } = useQuery(GET_FLOWS, {
|
||||
variables: {
|
||||
appKey,
|
||||
connectionId,
|
||||
...getLimitAndOffset(page),
|
||||
},
|
||||
});
|
||||
const getFlows = data?.getFlows || {};
|
||||
const { pageInfo, edges } = getFlows;
|
||||
|
||||
@@ -53,19 +55,21 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement {
|
||||
<AppFlowRow key={appFlow.id} flow={appFlow} />
|
||||
))}
|
||||
|
||||
{pageInfo && pageInfo.totalPages > 1 && <Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
onChange={(event, page) => setSearchParams({ page: page.toString() })}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>}
|
||||
{pageInfo && pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
onChange={(event, page) => setSearchParams({ page: page.toString() })}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
};
|
||||
);
|
||||
}
|
||||
|
@@ -13,15 +13,10 @@ const inlineImgStyle: React.CSSProperties = {
|
||||
objectFit: 'contain',
|
||||
};
|
||||
|
||||
export default function AppIcon(props: AppIconProps & AvatarProps): React.ReactElement {
|
||||
const {
|
||||
name,
|
||||
url,
|
||||
color,
|
||||
sx = {},
|
||||
variant = "square",
|
||||
...restProps
|
||||
} = props;
|
||||
export default function AppIcon(
|
||||
props: AppIconProps & AvatarProps
|
||||
): React.ReactElement {
|
||||
const { name, url, color, sx = {}, variant = 'square', ...restProps } = props;
|
||||
|
||||
const initialLetter = name?.[0];
|
||||
|
||||
@@ -37,4 +32,4 @@ export default function AppIcon(props: AppIconProps & AvatarProps): React.ReactE
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -14,20 +14,19 @@ import { CardContent, Typography } from './style';
|
||||
|
||||
type AppRowProps = {
|
||||
application: IApp;
|
||||
}
|
||||
};
|
||||
|
||||
const countTranslation = (value: React.ReactNode) => (
|
||||
<>
|
||||
<Typography variant="body1">
|
||||
{value}
|
||||
</Typography>
|
||||
<Typography variant="body1">{value}</Typography>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
|
||||
function AppRow(props: AppRowProps): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { name, primaryColor, iconUrl, connectionCount, flowCount } = props.application;
|
||||
const { name, primaryColor, iconUrl, connectionCount, flowCount } =
|
||||
props.application;
|
||||
|
||||
return (
|
||||
<Link to={URLS.APP(name.toLowerCase())} data-test="app-row">
|
||||
@@ -39,25 +38,37 @@ function AppRow(props: AppRowProps): React.ReactElement {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="h6">
|
||||
{name}
|
||||
<Typography variant="h6">{name}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
sx={{ display: ['none', 'inline-block'] }}
|
||||
>
|
||||
{formatMessage('app.connectionCount', {
|
||||
count: countTranslation(connectionCount),
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
|
||||
{formatMessage('app.connectionCount', { count: countTranslation(connectionCount) })}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Typography variant="caption" color="textSecondary" sx={{ display: ['none', 'inline-block'] }}>
|
||||
{formatMessage('app.flowCount', { count: countTranslation(flowCount) })}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
sx={{ display: ['none', 'inline-block'] }}
|
||||
>
|
||||
{formatMessage('app.flowCount', {
|
||||
count: countTranslation(flowCount),
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<ArrowForwardIosIcon sx={{ color: (theme) => theme.palette.primary.main }} />
|
||||
<ArrowForwardIosIcon
|
||||
sx={{ color: (theme) => theme.palette.primary.main }}
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
|
@@ -10,7 +10,6 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
|
||||
export const Typography = styled(MuiTypography)(() => ({
|
||||
'&.MuiTypography-h6': {
|
||||
textTransform: 'capitalize',
|
||||
@@ -22,5 +21,5 @@ export const Typography = styled(MuiTypography)(() => ({
|
||||
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
display: 'none',
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
@@ -12,7 +12,13 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||
import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||
import type { IApp, IStep, ISubstep, ITrigger, IAction } from '@automatisch/types';
|
||||
import type {
|
||||
IApp,
|
||||
IStep,
|
||||
ISubstep,
|
||||
ITrigger,
|
||||
IAction,
|
||||
} from '@automatisch/types';
|
||||
|
||||
type ChooseAppAndEventSubstepProps = {
|
||||
substep: ISubstep;
|
||||
@@ -24,7 +30,10 @@ type ChooseAppAndEventSubstepProps = {
|
||||
step: IStep;
|
||||
};
|
||||
|
||||
const optionGenerator = (app: { name: string, key: string, }): { label: string; value: string } => ({
|
||||
const optionGenerator = (app: {
|
||||
name: string;
|
||||
key: string;
|
||||
}): { label: string; value: string } => ({
|
||||
label: app.name as string,
|
||||
value: app.key as string,
|
||||
});
|
||||
@@ -61,15 +70,15 @@ function ChooseAppAndEventSubstep(
|
||||
() => apps?.map((app) => optionGenerator(app)),
|
||||
[apps]
|
||||
);
|
||||
const actionsOrTriggers: Array<ITrigger | IAction> = (isTrigger ? app?.triggers : app?.actions) || [];
|
||||
const actionsOrTriggers: Array<ITrigger | IAction> =
|
||||
(isTrigger ? app?.triggers : app?.actions) || [];
|
||||
const actionOptions = React.useMemo(
|
||||
() => actionsOrTriggers.map((trigger) => optionGenerator(trigger)),
|
||||
[app?.key]
|
||||
);
|
||||
const selectedActionOrTrigger =
|
||||
actionsOrTriggers.find(
|
||||
(actionOrTrigger: IAction | ITrigger) => actionOrTrigger.key === step?.key
|
||||
);
|
||||
const selectedActionOrTrigger = actionsOrTriggers.find(
|
||||
(actionOrTrigger: IAction | ITrigger) => actionOrTrigger.key === step?.key
|
||||
);
|
||||
|
||||
const { name } = substep;
|
||||
|
||||
|
@@ -16,7 +16,7 @@ import { TEST_CONNECTION } from 'graphql/queries/test-connection';
|
||||
|
||||
type ChooseConnectionSubstepProps = {
|
||||
application: IApp;
|
||||
substep: ISubstep,
|
||||
substep: ISubstep;
|
||||
expanded?: boolean;
|
||||
onExpand: () => void;
|
||||
onCollapse: () => void;
|
||||
@@ -27,14 +27,19 @@ type ChooseConnectionSubstepProps = {
|
||||
|
||||
const ADD_CONNECTION_VALUE = 'ADD_CONNECTION';
|
||||
|
||||
const optionGenerator = (connection: IConnection): { label: string; value: string; } => ({
|
||||
label: connection?.formattedData?.screenName as string ?? 'Unnamed',
|
||||
const optionGenerator = (
|
||||
connection: IConnection
|
||||
): { label: string; value: string } => ({
|
||||
label: (connection?.formattedData?.screenName as string) ?? 'Unnamed',
|
||||
value: connection?.id as string,
|
||||
});
|
||||
|
||||
const getOption = (options: Record<string, unknown>[], connectionId?: string) => options.find(connection => connection.value === connectionId) || null;
|
||||
const getOption = (options: Record<string, unknown>[], connectionId?: string) =>
|
||||
options.find((connection) => connection.value === connectionId) || null;
|
||||
|
||||
function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.ReactElement {
|
||||
function ChooseConnectionSubstep(
|
||||
props: ChooseConnectionSubstepProps
|
||||
): React.ReactElement {
|
||||
const {
|
||||
substep,
|
||||
expanded = false,
|
||||
@@ -45,36 +50,30 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
|
||||
onChange,
|
||||
application,
|
||||
} = props;
|
||||
const {
|
||||
connection,
|
||||
appKey,
|
||||
} = step;
|
||||
const { connection, appKey } = step;
|
||||
const formatMessage = useFormatMessage();
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
const [showAddConnectionDialog, setShowAddConnectionDialog] = React.useState(false);
|
||||
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, { variables: { key: appKey }});
|
||||
const [showAddConnectionDialog, setShowAddConnectionDialog] =
|
||||
React.useState(false);
|
||||
const { data, loading, refetch } = useQuery(GET_APP_CONNECTIONS, {
|
||||
variables: { key: appKey },
|
||||
});
|
||||
// TODO: show detailed error when connection test/verification fails
|
||||
const [
|
||||
testConnection,
|
||||
{
|
||||
loading: testResultLoading,
|
||||
refetch: retestConnection
|
||||
}
|
||||
] = useLazyQuery(
|
||||
TEST_CONNECTION,
|
||||
{
|
||||
variables: {
|
||||
id: connection?.id,
|
||||
}
|
||||
}
|
||||
);
|
||||
{ loading: testResultLoading, refetch: retestConnection },
|
||||
] = useLazyQuery(TEST_CONNECTION, {
|
||||
variables: {
|
||||
id: connection?.id,
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connection?.id) {
|
||||
testConnection({
|
||||
variables: {
|
||||
id: connection.id,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
// intentionally no dependencies for initial test
|
||||
@@ -82,52 +81,30 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
|
||||
|
||||
const connectionOptions = React.useMemo(() => {
|
||||
const appWithConnections = data?.getApp as IApp;
|
||||
const options = appWithConnections
|
||||
?.connections
|
||||
?.map((connection) => optionGenerator(connection)) || [];
|
||||
const options =
|
||||
appWithConnections?.connections?.map((connection) =>
|
||||
optionGenerator(connection)
|
||||
) || [];
|
||||
|
||||
options.push({
|
||||
label: formatMessage('chooseConnectionSubstep.addNewConnection'),
|
||||
value: ADD_CONNECTION_VALUE
|
||||
})
|
||||
value: ADD_CONNECTION_VALUE,
|
||||
});
|
||||
|
||||
return options;
|
||||
}, [data, formatMessage]);
|
||||
|
||||
const { name } = substep;
|
||||
|
||||
const handleAddConnectionClose = React.useCallback(async (response) => {
|
||||
setShowAddConnectionDialog(false);
|
||||
const handleAddConnectionClose = React.useCallback(
|
||||
async (response) => {
|
||||
setShowAddConnectionDialog(false);
|
||||
|
||||
const connectionId = response?.createConnection.id;
|
||||
const connectionId = response?.createConnection.id;
|
||||
|
||||
if (connectionId) {
|
||||
await refetch();
|
||||
if (connectionId) {
|
||||
await refetch();
|
||||
|
||||
onChange({
|
||||
step: {
|
||||
...step,
|
||||
connection: {
|
||||
id: connectionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [onChange, refetch, step]);
|
||||
|
||||
const handleChange = React.useCallback((event: React.SyntheticEvent, selectedOption: unknown) => {
|
||||
if (typeof selectedOption === 'object') {
|
||||
// TODO: try to simplify type casting below.
|
||||
const typedSelectedOption = selectedOption as { value: string };
|
||||
const option: { value: string } = typedSelectedOption;
|
||||
const connectionId = option?.value as string;
|
||||
|
||||
if (connectionId === ADD_CONNECTION_VALUE) {
|
||||
setShowAddConnectionDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionId !== step.connection?.id) {
|
||||
onChange({
|
||||
step: {
|
||||
...step,
|
||||
@@ -137,8 +114,37 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [step, onChange]);
|
||||
},
|
||||
[onChange, refetch, step]
|
||||
);
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(event: React.SyntheticEvent, selectedOption: unknown) => {
|
||||
if (typeof selectedOption === 'object') {
|
||||
// TODO: try to simplify type casting below.
|
||||
const typedSelectedOption = selectedOption as { value: string };
|
||||
const option: { value: string } = typedSelectedOption;
|
||||
const connectionId = option?.value as string;
|
||||
|
||||
if (connectionId === ADD_CONNECTION_VALUE) {
|
||||
setShowAddConnectionDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionId !== step.connection?.id) {
|
||||
onChange({
|
||||
step: {
|
||||
...step,
|
||||
connection: {
|
||||
id: connectionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[step, onChange]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (step.connection?.id) {
|
||||
@@ -146,7 +152,7 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
|
||||
id: step.connection.id,
|
||||
});
|
||||
}
|
||||
}, [step.connection?.id, retestConnection])
|
||||
}, [step.connection?.id, retestConnection]);
|
||||
|
||||
const onToggle = expanded ? onCollapse : onExpand;
|
||||
|
||||
@@ -159,7 +165,14 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
|
||||
valid={testResultLoading ? null : connection?.verified}
|
||||
/>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<ListItem
|
||||
sx={{
|
||||
pt: 2,
|
||||
pb: 3,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
disablePortal
|
||||
@@ -169,7 +182,9 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={formatMessage('chooseConnectionSubstep.chooseConnection')}
|
||||
label={formatMessage(
|
||||
'chooseConnectionSubstep.chooseConnection'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
value={getOption(connectionOptions, connection?.id)}
|
||||
@@ -183,17 +198,24 @@ function ChooseConnectionSubstep(props: ChooseConnectionSubstepProps): React.Rea
|
||||
variant="contained"
|
||||
onClick={onSubmit}
|
||||
sx={{ mt: 2 }}
|
||||
disabled={testResultLoading || !connection?.verified || editorContext.readOnly}data-test="flow-substep-continue-button"
|
||||
disabled={
|
||||
testResultLoading ||
|
||||
!connection?.verified ||
|
||||
editorContext.readOnly
|
||||
}
|
||||
data-test="flow-substep-continue-button"
|
||||
>
|
||||
{formatMessage('chooseConnectionSubstep.continue')}
|
||||
</Button>
|
||||
</ListItem>
|
||||
</Collapse>
|
||||
|
||||
{application && showAddConnectionDialog && <AddAppConnection
|
||||
onClose={handleAddConnectionClose}
|
||||
application={application}
|
||||
/>}
|
||||
{application && showAddConnectionDialog && (
|
||||
<AddAppConnection
|
||||
onClose={handleAddConnectionClose}
|
||||
application={application}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@@ -10,7 +10,9 @@ import { IconButton } from './style';
|
||||
export default function ConditionalIconButton(props: any): React.ReactElement {
|
||||
const { icon, ...buttonProps } = props;
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
||||
noSsr: true,
|
||||
});
|
||||
|
||||
if (matchSmallScreens) {
|
||||
return (
|
||||
@@ -22,10 +24,8 @@ export default function ConditionalIconButton(props: any): React.ReactElement {
|
||||
>
|
||||
{icon}
|
||||
</IconButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button {...(buttonProps as ButtonProps)} />
|
||||
);
|
||||
return <Button {...(buttonProps as ButtonProps)} />;
|
||||
}
|
||||
|
@@ -2,11 +2,9 @@ import * as React from 'react';
|
||||
import MuiContainer, { ContainerProps } from '@mui/material/Container';
|
||||
|
||||
export default function Container(props: ContainerProps): React.ReactElement {
|
||||
return (
|
||||
<MuiContainer {...props} />
|
||||
);
|
||||
};
|
||||
return <MuiContainer {...props} />;
|
||||
}
|
||||
|
||||
Container.defaultProps = {
|
||||
maxWidth: 'lg'
|
||||
maxWidth: 'lg',
|
||||
};
|
||||
|
@@ -4,7 +4,8 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
import Autocomplete, { AutocompleteProps } from '@mui/material/Autocomplete';
|
||||
import type { IFieldDropdownOption } from '@automatisch/types';
|
||||
|
||||
interface ControlledAutocompleteProps extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
||||
interface ControlledAutocompleteProps
|
||||
extends AutocompleteProps<IFieldDropdownOption, boolean, boolean, boolean> {
|
||||
shouldUnregister?: boolean;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
@@ -12,9 +13,12 @@ interface ControlledAutocompleteProps extends AutocompleteProps<IFieldDropdownOp
|
||||
dependsOn?: string[];
|
||||
}
|
||||
|
||||
const getOption = (options: readonly IFieldDropdownOption[], value: string) => options.find(option => option.value === value) || null;
|
||||
const getOption = (options: readonly IFieldDropdownOption[], value: string) =>
|
||||
options.find((option) => option.value === value) || null;
|
||||
|
||||
function ControlledAutocomplete(props: ControlledAutocompleteProps): React.ReactElement {
|
||||
function ControlledAutocomplete(
|
||||
props: ControlledAutocompleteProps
|
||||
): React.ReactElement {
|
||||
const { control, watch, setValue, resetField } = useFormContext();
|
||||
|
||||
const {
|
||||
@@ -53,7 +57,15 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
||||
defaultValue={defaultValue || ''}
|
||||
control={control}
|
||||
shouldUnregister={shouldUnregister}
|
||||
render={({ field: { ref, onChange: controllerOnChange, onBlur: controllerOnBlur, ...field }, fieldState }) => (
|
||||
render={({
|
||||
field: {
|
||||
ref,
|
||||
onChange: controllerOnChange,
|
||||
onBlur: controllerOnBlur,
|
||||
...field
|
||||
},
|
||||
fieldState,
|
||||
}) => (
|
||||
<div>
|
||||
{/* encapsulated with an element such as div to vertical spacing delegated from parent */}
|
||||
<Autocomplete
|
||||
@@ -62,8 +74,15 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
||||
options={options}
|
||||
value={getOption(options, field.value)}
|
||||
onChange={(event, selectedOption, reason, details) => {
|
||||
const typedSelectedOption = selectedOption as IFieldDropdownOption;
|
||||
if (typedSelectedOption !== null && Object.prototype.hasOwnProperty.call(typedSelectedOption, 'value')) {
|
||||
const typedSelectedOption =
|
||||
selectedOption as IFieldDropdownOption;
|
||||
if (
|
||||
typedSelectedOption !== null &&
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
typedSelectedOption,
|
||||
'value'
|
||||
)
|
||||
) {
|
||||
controllerOnChange(typedSelectedOption.value);
|
||||
} else {
|
||||
controllerOnChange(typedSelectedOption);
|
||||
@@ -71,7 +90,10 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
||||
|
||||
onChange?.(event, selectedOption, reason, details);
|
||||
}}
|
||||
onBlur={(...args) => { controllerOnBlur(); onBlur?.(...args); }}
|
||||
onBlur={(...args) => {
|
||||
controllerOnBlur();
|
||||
onBlur?.(...args);
|
||||
}}
|
||||
ref={ref}
|
||||
data-test={`${name}-autocomplete`}
|
||||
/>
|
||||
@@ -80,7 +102,9 @@ function ControlledAutocomplete(props: ControlledAutocompleteProps): React.React
|
||||
variant="outlined"
|
||||
error={Boolean(fieldState.isTouched && fieldState.error)}
|
||||
>
|
||||
{fieldState.isTouched ? fieldState.error?.message || description : description}
|
||||
{fieldState.isTouched
|
||||
? fieldState.error?.message || description
|
||||
: description}
|
||||
</FormHelperText>
|
||||
</div>
|
||||
)}
|
||||
|
@@ -11,7 +11,9 @@ import ListItemLink from 'components/ListItemLink';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { Drawer as BaseDrawer } from './style';
|
||||
|
||||
const iOS = typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
const iOS =
|
||||
typeof navigator !== 'undefined' &&
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
||||
|
||||
type DrawerLink = {
|
||||
Icon: React.ElementType;
|
||||
@@ -29,14 +31,16 @@ type DrawerProps = {
|
||||
export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||
const { links = [], bottomLinks = [], ...drawerProps } = props;
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
||||
noSsr: true,
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const closeOnClick = (event: React.SyntheticEvent) => {
|
||||
if (matchSmallScreens) {
|
||||
props.onClose(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDrawer
|
||||
@@ -69,11 +73,11 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
|
||||
{bottomLinks.map(({ Icon, badgeContent, primary, to }, index) => (
|
||||
<ListItemLink
|
||||
key={`${to}-${index}`}
|
||||
icon={(
|
||||
icon={
|
||||
<Badge badgeContent={badgeContent} color="secondary" max={99}>
|
||||
<Icon htmlColor={theme.palette.primary.main} />
|
||||
</Badge>
|
||||
)}
|
||||
}
|
||||
primary={formatMessage(primary)}
|
||||
to={to}
|
||||
onClick={closeOnClick}
|
||||
|
@@ -28,27 +28,25 @@ const closedMixin = (theme: Theme): CSSObject => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const Drawer = styled(MuiSwipeableDrawer)(
|
||||
({ theme, open }) => ({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
...(open && {
|
||||
export const Drawer = styled(MuiSwipeableDrawer)(({ theme, open }) => ({
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
whiteSpace: 'nowrap',
|
||||
boxSizing: 'border-box',
|
||||
...(open && {
|
||||
...openedMixin(theme),
|
||||
[`& .${drawerClasses.paper}`]: {
|
||||
...openedMixin(theme),
|
||||
[`& .${drawerClasses.paper}`]: {
|
||||
...openedMixin(theme),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
}),
|
||||
...(!open && {
|
||||
...closedMixin(theme),
|
||||
[`& .${drawerClasses.paper}`]: {
|
||||
...closedMixin(theme),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
}),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}),
|
||||
);
|
||||
...(!open && {
|
||||
...closedMixin(theme),
|
||||
[`& .${drawerClasses.paper}`]: {
|
||||
...closedMixin(theme),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import type { TypographyProps } from '@mui/material/Typography';
|
||||
import type { TypographyProps } from '@mui/material/Typography';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
||||
import { Box, TextField } from './style';
|
||||
@@ -17,40 +17,45 @@ function EditableTypography(props: EditableTypographyProps) {
|
||||
const [editing, setEditing] = React.useState(false);
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
setEditing(editing => !editing);
|
||||
setEditing((editing) => !editing);
|
||||
}, []);
|
||||
|
||||
const handleTextFieldClick = React.useCallback((event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
const handleTextFieldClick = React.useCallback(
|
||||
(event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleTextFieldKeyDown = React.useCallback(async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (event.key === 'Enter') {
|
||||
if (target.value !== children) {
|
||||
await onConfirm(target.value);
|
||||
const handleTextFieldKeyDown = React.useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
if (event.key === 'Enter') {
|
||||
if (target.value !== children) {
|
||||
await onConfirm(target.value);
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
}
|
||||
},
|
||||
[children]
|
||||
);
|
||||
|
||||
const handleTextFieldBlur = React.useCallback(
|
||||
async (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
|
||||
if (value !== children) {
|
||||
await onConfirm(value);
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
}
|
||||
}, [children]);
|
||||
|
||||
const handleTextFieldBlur = React.useCallback(async (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
|
||||
if (value !== children) {
|
||||
await onConfirm(value);
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
}, [onConfirm, children]);
|
||||
|
||||
let component = (
|
||||
<Typography {...typographyProps}>
|
||||
{children}
|
||||
</Typography>
|
||||
},
|
||||
[onConfirm, children]
|
||||
);
|
||||
|
||||
let component = <Typography {...typographyProps}>{children}</Typography>;
|
||||
|
||||
if (editing) {
|
||||
component = (
|
||||
<TextField
|
||||
@@ -62,7 +67,7 @@ function EditableTypography(props: EditableTypographyProps) {
|
||||
defaultValue={children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={sx} onClick={handleClick} editing={editing}>
|
||||
|
@@ -5,10 +5,12 @@ import { inputClasses } from '@mui/material/Input';
|
||||
|
||||
type BoxProps = {
|
||||
editing?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const boxShouldForwardProp = (prop: string) => !['editing'].includes(prop);
|
||||
export const Box = styled(MuiBox, { shouldForwardProp: boxShouldForwardProp })<BoxProps>`
|
||||
export const Box = styled(MuiBox, {
|
||||
shouldForwardProp: boxShouldForwardProp,
|
||||
})<BoxProps>`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 300px;
|
||||
@@ -19,7 +21,8 @@ export const Box = styled(MuiBox, { shouldForwardProp: boxShouldForwardProp })<B
|
||||
|
||||
export const TextField = styled(MuiTextField)({
|
||||
width: '100%',
|
||||
[`.${inputClasses.root}:before, .${inputClasses.root}:after, .${inputClasses.root}:hover`]: {
|
||||
borderBottom: '0 !important',
|
||||
}
|
||||
[`.${inputClasses.root}:before, .${inputClasses.root}:after, .${inputClasses.root}:hover`]:
|
||||
{
|
||||
borderBottom: '0 !important',
|
||||
},
|
||||
});
|
||||
|
@@ -40,16 +40,19 @@ function updateHandlerFactory(flowId: string, previousStepId: string) {
|
||||
|
||||
export default function Editor(props: EditorProps): React.ReactElement {
|
||||
const [updateStep] = useMutation(UPDATE_STEP);
|
||||
const [createStep, { loading: creationInProgress }] = useMutation(CREATE_STEP, {
|
||||
refetchQueries: [
|
||||
'GetFlow'
|
||||
]
|
||||
});
|
||||
const [createStep, { loading: creationInProgress }] = useMutation(
|
||||
CREATE_STEP,
|
||||
{
|
||||
refetchQueries: ['GetFlow'],
|
||||
}
|
||||
);
|
||||
|
||||
const { flow } = props;
|
||||
const [triggerStep] = flow.steps;
|
||||
|
||||
const [currentStepId, setCurrentStepId] = React.useState<string | null>(triggerStep.id);
|
||||
const [currentStepId, setCurrentStepId] = React.useState<string | null>(
|
||||
triggerStep.id
|
||||
);
|
||||
|
||||
const onStepChange = React.useCallback(
|
||||
(step: any) => {
|
||||
@@ -125,7 +128,11 @@ export default function Editor(props: EditorProps): React.ReactElement {
|
||||
onContinue={openNextStep(steps[index + 1])}
|
||||
/>
|
||||
|
||||
<IconButton onClick={() => addStep(step.id)} color="primary" disabled={creationInProgress || flow.active}>
|
||||
<IconButton
|
||||
onClick={() => addStep(step.id)}
|
||||
color="primary"
|
||||
disabled={creationInProgress || flow.active}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
|
@@ -25,51 +25,69 @@ export default function EditorLayout(): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const [updateFlow] = useMutation(UPDATE_FLOW);
|
||||
const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS);
|
||||
const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId }});
|
||||
const { data, loading } = useQuery(GET_FLOW, { variables: { id: flowId } });
|
||||
const flow: IFlow = data?.getFlow;
|
||||
|
||||
const onFlowNameUpdate = React.useCallback(async (name: string) => {
|
||||
await updateFlow({
|
||||
variables: {
|
||||
input: {
|
||||
id: flowId,
|
||||
name,
|
||||
const onFlowNameUpdate = React.useCallback(
|
||||
async (name: string) => {
|
||||
await updateFlow({
|
||||
variables: {
|
||||
input: {
|
||||
id: flowId,
|
||||
name,
|
||||
},
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateFlow: {
|
||||
__typename: 'Flow',
|
||||
id: flow?.id,
|
||||
name,
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [flow?.id]);
|
||||
optimisticResponse: {
|
||||
updateFlow: {
|
||||
__typename: 'Flow',
|
||||
id: flow?.id,
|
||||
name,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[flow?.id]
|
||||
);
|
||||
|
||||
const onFlowStatusUpdate = React.useCallback(async (active: boolean) => {
|
||||
await updateFlowStatus({
|
||||
variables: {
|
||||
input: {
|
||||
id: flowId,
|
||||
active,
|
||||
const onFlowStatusUpdate = React.useCallback(
|
||||
async (active: boolean) => {
|
||||
await updateFlowStatus({
|
||||
variables: {
|
||||
input: {
|
||||
id: flowId,
|
||||
active,
|
||||
},
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateFlowStatus: {
|
||||
__typename: 'Flow',
|
||||
id: flow?.id,
|
||||
active,
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [flow?.id]);
|
||||
optimisticResponse: {
|
||||
updateFlowStatus: {
|
||||
__typename: 'Flow',
|
||||
id: flow?.id,
|
||||
active,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[flow?.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" height="100%">
|
||||
<Stack direction="row" bgcolor="white" justifyContent="space-between" alignItems="center" boxShadow={1} py={1} px={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
bgcolor="white"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
boxShadow={1}
|
||||
py={1}
|
||||
px={1}
|
||||
>
|
||||
<Box display="flex" flex={1} alignItems="center">
|
||||
<Tooltip placement="right" title={formatMessage('flowEditor.goBack')} disableInteractive>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={formatMessage('flowEditor.goBack')}
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
component={Link}
|
||||
@@ -97,9 +115,13 @@ export default function EditorLayout(): React.ReactElement {
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => onFlowStatusUpdate(!flow.active)}
|
||||
data-test={flow?.active ? 'unpublish-flow-button' : 'publish-flow-button'}
|
||||
data-test={
|
||||
flow?.active ? 'unpublish-flow-button' : 'publish-flow-button'
|
||||
}
|
||||
>
|
||||
{flow?.active ? formatMessage('flowEditor.unpublish') : formatMessage('flowEditor.publish')}
|
||||
{flow?.active
|
||||
? formatMessage('flowEditor.unpublish')
|
||||
: formatMessage('flowEditor.publish')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -118,8 +140,8 @@ export default function EditorLayout(): React.ReactElement {
|
||||
open={!!flow?.active}
|
||||
message={formatMessage('flowEditor.publishedFlowCannotBeUpdated')}
|
||||
anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
|
||||
ContentProps={{ sx: { fontWeight: 300 }}}
|
||||
action={(
|
||||
ContentProps={{ sx: { fontWeight: 300 } }}
|
||||
action={
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
@@ -128,8 +150,8 @@ export default function EditorLayout(): React.ReactElement {
|
||||
>
|
||||
{formatMessage('flowEditor.unpublish')}
|
||||
</Button>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -10,43 +10,53 @@ type ExecutionHeaderProps = {
|
||||
execution: IExecution;
|
||||
};
|
||||
|
||||
function ExecutionName(props: Pick<IExecution["flow"], "name">) {
|
||||
function ExecutionName(props: Pick<IExecution['flow'], 'name'>) {
|
||||
return (
|
||||
<Typography variant="h3" gutterBottom>{props.name}</Typography>
|
||||
<Typography variant="h3" gutterBottom>
|
||||
{props.name}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
function ExecutionId(props: Pick<IExecution, "id">) {
|
||||
function ExecutionId(props: Pick<IExecution, 'id'>) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Typography variant="body2">
|
||||
Execution ID: <Typography variant="body1" component="span">{props.id}</Typography>
|
||||
Execution ID:{' '}
|
||||
<Typography variant="body1" component="span">
|
||||
{props.id}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ExecutionDate(props: Pick<IExecution, "createdAt">) {
|
||||
const createdAt = DateTime.fromMillis(
|
||||
parseInt(props.createdAt, 10)
|
||||
);
|
||||
function ExecutionDate(props: Pick<IExecution, 'createdAt'>) {
|
||||
const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10));
|
||||
const relativeCreatedAt = createdAt.toRelative();
|
||||
|
||||
return (
|
||||
<Tooltip title={createdAt.toLocaleString(DateTime.DATE_MED)}>
|
||||
<Typography variant="body1" gutterBottom>{relativeCreatedAt}</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{relativeCreatedAt}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ExecutionHeader(props: ExecutionHeaderProps): React.ReactElement {
|
||||
export default function ExecutionHeader(
|
||||
props: ExecutionHeaderProps
|
||||
): React.ReactElement {
|
||||
const { execution } = props;
|
||||
|
||||
if (!execution) return <React.Fragment />;
|
||||
|
||||
return (
|
||||
<Stack direction="column">
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between">
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<ExecutionDate createdAt={execution.createdAt} />
|
||||
<ExecutionId id={execution.id} />
|
||||
</Stack>
|
||||
|
@@ -14,9 +14,11 @@ import { Apps, CardContent, ArrowContainer, Title, Typography } from './style';
|
||||
|
||||
type ExecutionRowProps = {
|
||||
execution: IExecution;
|
||||
}
|
||||
};
|
||||
|
||||
export default function ExecutionRow(props: ExecutionRowProps): React.ReactElement {
|
||||
export default function ExecutionRow(
|
||||
props: ExecutionRowProps
|
||||
): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const { execution } = props;
|
||||
const { flow } = execution;
|
||||
@@ -29,21 +31,19 @@ export default function ExecutionRow(props: ExecutionRowProps): React.ReactEleme
|
||||
<Card sx={{ mb: 1 }}>
|
||||
<CardActionArea>
|
||||
<CardContent>
|
||||
<Apps direction="row" gap={1} sx={{gridArea:"apps"}}>
|
||||
<Apps direction="row" gap={1} sx={{ gridArea: 'apps' }}>
|
||||
<FlowAppIcons steps={flow.steps} />
|
||||
</Apps>
|
||||
|
||||
<Title
|
||||
justifyContent="center"
|
||||
alignItems="flex-start"
|
||||
spacing={1}
|
||||
>
|
||||
<Title justifyContent="center" alignItems="flex-start" spacing={1}>
|
||||
<Typography variant="h6" noWrap>
|
||||
{flow.name}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" noWrap>
|
||||
{formatMessage('execution.executedAt', { datetime: relativeCreatedAt })}
|
||||
{formatMessage('execution.executedAt', {
|
||||
datetime: relativeCreatedAt,
|
||||
})}
|
||||
</Typography>
|
||||
</Title>
|
||||
|
||||
@@ -57,7 +57,9 @@ export default function ExecutionRow(props: ExecutionRowProps): React.ReactEleme
|
||||
/>
|
||||
)}
|
||||
|
||||
<ArrowForwardIosIcon sx={{ color: (theme) => theme.palette.primary.main }} />
|
||||
<ArrowForwardIosIcon
|
||||
sx={{ color: (theme) => theme.palette.primary.main }}
|
||||
/>
|
||||
</ArrowContainer>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
|
@@ -20,7 +20,7 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||
`,
|
||||
gridTemplateColumns: 'minmax(0, auto) min-content',
|
||||
gridTemplateRows: 'auto auto',
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export const Apps = styled(MuiStack)(() => ({
|
||||
@@ -46,5 +46,5 @@ export const Typography = styled(MuiTypography)(() => ({
|
||||
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
display: 'none',
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
@@ -14,32 +14,43 @@ import JSONViewer from 'components/JSONViewer';
|
||||
import AppIcon from 'components/AppIcon';
|
||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { AppIconWrapper, AppIconStatusIconWrapper, Content, Header, Wrapper } from './style';
|
||||
import {
|
||||
AppIconWrapper,
|
||||
AppIconStatusIconWrapper,
|
||||
Content,
|
||||
Header,
|
||||
Wrapper,
|
||||
} from './style';
|
||||
|
||||
type ExecutionStepProps = {
|
||||
collapsed?: boolean;
|
||||
step: IStep;
|
||||
index?: number;
|
||||
executionStep: IExecutionStep;
|
||||
}
|
||||
};
|
||||
|
||||
const validIcon = <CheckCircleIcon color="success" />;
|
||||
const errorIcon = <ErrorIcon color="error" />;
|
||||
|
||||
export default function ExecutionStep(props: ExecutionStepProps): React.ReactElement | null {
|
||||
const { executionStep, } = props;
|
||||
export default function ExecutionStep(
|
||||
props: ExecutionStepProps
|
||||
): React.ReactElement | null {
|
||||
const { executionStep } = props;
|
||||
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
|
||||
const step: IStep = executionStep.step;
|
||||
const isTrigger = step.type === 'trigger';
|
||||
const isAction = step.type === 'action';
|
||||
const formatMessage = useFormatMessage();
|
||||
const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger, onlyWithActions: isAction }});
|
||||
const { data } = useQuery(GET_APPS, {
|
||||
variables: { onlyWithTriggers: isTrigger, onlyWithActions: isAction },
|
||||
});
|
||||
const apps: IApp[] = data?.getApps;
|
||||
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
|
||||
|
||||
if (!apps) return null;
|
||||
if (!apps) return null;
|
||||
|
||||
const validationStatusIcon = executionStep.status === 'success' ? validIcon : errorIcon;
|
||||
const validationStatusIcon =
|
||||
executionStep.status === 'success' ? validIcon : errorIcon;
|
||||
|
||||
return (
|
||||
<Wrapper elevation={1} data-test="execution-step">
|
||||
@@ -55,11 +66,9 @@ export default function ExecutionStep(props: ExecutionStepProps): React.ReactEle
|
||||
|
||||
<div>
|
||||
<Typography variant="caption">
|
||||
{
|
||||
isTrigger ?
|
||||
formatMessage('flowStep.triggerType') :
|
||||
formatMessage('flowStep.actionType')
|
||||
}
|
||||
{isTrigger
|
||||
? formatMessage('flowStep.triggerType')
|
||||
: formatMessage('flowStep.actionType')}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2">
|
||||
@@ -71,7 +80,10 @@ export default function ExecutionStep(props: ExecutionStepProps): React.ReactEle
|
||||
|
||||
<Content sx={{ px: 2 }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={activeTabIndex} onChange={(event, tabIndex) => setActiveTabIndex(tabIndex)}>
|
||||
<Tabs
|
||||
value={activeTabIndex}
|
||||
onChange={(event, tabIndex) => setActiveTabIndex(tabIndex)}
|
||||
>
|
||||
<Tab label="Data in" data-test="data-in-tab" />
|
||||
<Tab label="Data out" data-test="data-out-tab" />
|
||||
<Tab label="Error" data-test="error-tab" />
|
||||
@@ -90,7 +102,6 @@ export default function ExecutionStep(props: ExecutionStepProps): React.ReactEle
|
||||
<JSONViewer data={executionStep.errorDetails} />
|
||||
</TabPanel>
|
||||
</Content>
|
||||
|
||||
</Wrapper>
|
||||
)
|
||||
};
|
||||
);
|
||||
}
|
||||
|
@@ -27,15 +27,17 @@ export const Wrapper = styled(Card)`
|
||||
|
||||
type HeaderProps = {
|
||||
collapsed?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const Header = styled('div', { shouldForwardProp: prop => prop !== 'collapsed' })<HeaderProps>`
|
||||
export const Header = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'collapsed',
|
||||
})<HeaderProps>`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
cursor: ${({ collapsed }) => collapsed ? 'pointer' : 'unset'};
|
||||
cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')};
|
||||
`;
|
||||
|
||||
export const Content = styled('div')`
|
||||
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, .8)};
|
||||
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)};
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
padding: ${({ theme }) => theme.spacing(2, 0)};
|
||||
|
@@ -6,7 +6,7 @@ import IntermediateStepCount from 'components/IntermediateStepCount';
|
||||
|
||||
type FlowAppIconsProps = {
|
||||
steps: Partial<IStep>[];
|
||||
}
|
||||
};
|
||||
|
||||
export default function FlowAppIcons(props: FlowAppIconsProps) {
|
||||
const { steps } = props;
|
||||
@@ -24,14 +24,18 @@ export default function FlowAppIcons(props: FlowAppIconsProps) {
|
||||
sx={{ width: 30, height: 30 }}
|
||||
/>
|
||||
|
||||
{intermeaditeStepCount > 0 && <IntermediateStepCount count={intermeaditeStepCount} />}
|
||||
{intermeaditeStepCount > 0 && (
|
||||
<IntermediateStepCount count={intermeaditeStepCount} />
|
||||
)}
|
||||
|
||||
{lastStep && <AppIcon
|
||||
name=" "
|
||||
variant="rounded"
|
||||
url={lastStep.iconUrl}
|
||||
sx={{ width: 30, height: 30 }}
|
||||
/>}
|
||||
{lastStep && (
|
||||
<AppIcon
|
||||
name=" "
|
||||
variant="rounded"
|
||||
url={lastStep.iconUrl}
|
||||
sx={{ width: 30, height: 30 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
};
|
||||
);
|
||||
}
|
||||
|
@@ -16,7 +16,9 @@ type ContextMenuProps = {
|
||||
anchorEl: PopoverProps['anchorEl'];
|
||||
};
|
||||
|
||||
export default function ContextMenu(props: ContextMenuProps): React.ReactElement {
|
||||
export default function ContextMenu(
|
||||
props: ContextMenuProps
|
||||
): React.ReactElement {
|
||||
const { flowId, onClose, anchorEl } = props;
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const [deleteFlow] = useMutation(DELETE_FLOW);
|
||||
@@ -34,10 +36,12 @@ export default function ContextMenu(props: ContextMenuProps): React.ReactElement
|
||||
cache.evict({
|
||||
id: flowCacheId,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), { variant: 'success' });
|
||||
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
|
||||
variant: 'success',
|
||||
});
|
||||
}, [flowId, deleteFlow]);
|
||||
|
||||
return (
|
||||
@@ -47,16 +51,11 @@ export default function ContextMenu(props: ContextMenuProps): React.ReactElement
|
||||
hideBackdrop={false}
|
||||
anchorEl={anchorEl}
|
||||
>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={URLS.FLOW(flowId)}
|
||||
>
|
||||
<MenuItem component={Link} to={URLS.FLOW(flowId)}>
|
||||
{formatMessage('flow.view')}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={onFlowDelete}>
|
||||
{formatMessage('flow.delete')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onFlowDelete}>{formatMessage('flow.delete')}</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -16,12 +16,14 @@ import { Apps, CardContent, ContextMenu, Title, Typography } from './style';
|
||||
|
||||
type FlowRowProps = {
|
||||
flow: IFlow;
|
||||
}
|
||||
};
|
||||
|
||||
export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
||||
null
|
||||
);
|
||||
const { flow } = props;
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -32,7 +34,7 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
setAnchorEl(contextButtonRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10));
|
||||
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10));
|
||||
@@ -45,7 +47,7 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
<Card sx={{ mb: 1 }}>
|
||||
<CardActionArea component={Link} to={URLS.FLOW(flow.id)}>
|
||||
<CardContent>
|
||||
<Apps direction="row" gap={1} sx={{gridArea:"apps"}}>
|
||||
<Apps direction="row" gap={1} sx={{ gridArea: 'apps' }}>
|
||||
<FlowAppIcons steps={flow.steps} />
|
||||
</Apps>
|
||||
|
||||
@@ -53,15 +55,21 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
justifyContent="center"
|
||||
alignItems="flex-start"
|
||||
spacing={1}
|
||||
sx={{gridArea:"title"}}
|
||||
sx={{ gridArea: 'title' }}
|
||||
>
|
||||
<Typography variant="h6" noWrap>
|
||||
{flow?.name}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption">
|
||||
{isUpdated && formatMessage('flow.updatedAt', { datetime: relativeUpdatedAt })}
|
||||
{!isUpdated && formatMessage('flow.createdAt', { datetime: relativeCreatedAt })}
|
||||
{isUpdated &&
|
||||
formatMessage('flow.updatedAt', {
|
||||
datetime: relativeUpdatedAt,
|
||||
})}
|
||||
{!isUpdated &&
|
||||
formatMessage('flow.createdAt', {
|
||||
datetime: relativeCreatedAt,
|
||||
})}
|
||||
</Typography>
|
||||
</Title>
|
||||
|
||||
@@ -69,8 +77,10 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
<Chip
|
||||
size="small"
|
||||
color={flow?.active ? 'success' : 'info'}
|
||||
variant={flow?.active ? 'filled': 'outlined'}
|
||||
label={formatMessage(flow?.active ? 'flow.published' : 'flow.draft')}
|
||||
variant={flow?.active ? 'filled' : 'outlined'}
|
||||
label={formatMessage(
|
||||
flow?.active ? 'flow.published' : 'flow.draft'
|
||||
)}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
@@ -88,11 +98,13 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
|
||||
{anchorEl && <FlowContextMenu
|
||||
flowId={flow.id}
|
||||
onClose={handleClose}
|
||||
anchorEl={anchorEl}
|
||||
/>}
|
||||
{anchorEl && (
|
||||
<FlowContextMenu
|
||||
flowId={flow.id}
|
||||
onClose={handleClose}
|
||||
anchorEl={anchorEl}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ export const CardContent = styled(MuiCardContent)(({ theme }) => ({
|
||||
`,
|
||||
gridTemplateColumns: 'minmax(0, auto) min-content',
|
||||
gridTemplateRows: 'auto auto',
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export const Apps = styled(MuiStack)(() => ({
|
||||
@@ -46,5 +46,5 @@ export const Typography = styled(MuiTypography)(() => ({
|
||||
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
display: 'none',
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
@@ -13,7 +13,13 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
import type { BaseSchema } from 'yup';
|
||||
import type { IApp, ITrigger, IAction, IStep, ISubstep } from '@automatisch/types';
|
||||
import type {
|
||||
IApp,
|
||||
ITrigger,
|
||||
IAction,
|
||||
IStep,
|
||||
ISubstep,
|
||||
} from '@automatisch/types';
|
||||
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||
@@ -49,53 +55,63 @@ const validIcon = <CheckCircleIcon color="success" />;
|
||||
const errorIcon = <ErrorIcon color="error" />;
|
||||
|
||||
function generateValidationSchema(substeps: ISubstep[]) {
|
||||
const fieldValidations = substeps?.reduce((allValidations, { arguments: args }) => {
|
||||
if (!args || !Array.isArray(args)) return allValidations;
|
||||
const fieldValidations = substeps?.reduce(
|
||||
(allValidations, { arguments: args }) => {
|
||||
if (!args || !Array.isArray(args)) return allValidations;
|
||||
|
||||
const substepArgumentValidations: Record<string, BaseSchema> = {};
|
||||
const substepArgumentValidations: Record<string, BaseSchema> = {};
|
||||
|
||||
for (const arg of args) {
|
||||
const { key, required, dependsOn } = arg;
|
||||
for (const arg of args) {
|
||||
const { key, required, dependsOn } = arg;
|
||||
|
||||
// base validation for the field if not exists
|
||||
if (!substepArgumentValidations[key]) {
|
||||
substepArgumentValidations[key] = yup.mixed();
|
||||
}
|
||||
|
||||
if (typeof substepArgumentValidations[key] === 'object') {
|
||||
// if the field is required, add the required validation
|
||||
if (required) {
|
||||
substepArgumentValidations[key] = substepArgumentValidations[key].required(`${key} is required.`);
|
||||
// base validation for the field if not exists
|
||||
if (!substepArgumentValidations[key]) {
|
||||
substepArgumentValidations[key] = yup.mixed();
|
||||
}
|
||||
|
||||
// if the field depends on another field, add the dependsOn required validation
|
||||
if (Array.isArray(dependsOn) && dependsOn.length > 0) {
|
||||
for (const dependsOnKey of dependsOn) {
|
||||
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
||||
if (typeof substepArgumentValidations[key] === 'object') {
|
||||
// if the field is required, add the required validation
|
||||
if (required) {
|
||||
substepArgumentValidations[key] = substepArgumentValidations[
|
||||
key
|
||||
].required(`${key} is required.`);
|
||||
}
|
||||
|
||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||
substepArgumentValidations[key] = substepArgumentValidations[key].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
||||
is: (value: string) => Boolean(value) === false,
|
||||
then: (schema) => schema.notOneOf([''], missingDependencyValueMessage).required(missingDependencyValueMessage),
|
||||
});
|
||||
// if the field depends on another field, add the dependsOn required validation
|
||||
if (Array.isArray(dependsOn) && dependsOn.length > 0) {
|
||||
for (const dependsOnKey of dependsOn) {
|
||||
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
||||
|
||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||
substepArgumentValidations[key] = substepArgumentValidations[
|
||||
key
|
||||
].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
||||
is: (value: string) => Boolean(value) === false,
|
||||
then: (schema) =>
|
||||
schema
|
||||
.notOneOf([''], missingDependencyValueMessage)
|
||||
.required(missingDependencyValueMessage),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...allValidations,
|
||||
...substepArgumentValidations,
|
||||
}
|
||||
}, {});
|
||||
return {
|
||||
...allValidations,
|
||||
...substepArgumentValidations,
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const validationSchema = yup.object({
|
||||
parameters: yup.object(fieldValidations),
|
||||
});
|
||||
|
||||
return yupResolver(validationSchema);
|
||||
};
|
||||
}
|
||||
|
||||
export default function FlowStep(
|
||||
props: FlowStepProps
|
||||
@@ -140,10 +156,12 @@ export default function FlowStep(
|
||||
const apps: IApp[] = data?.getApps;
|
||||
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
|
||||
|
||||
const actionsOrTriggers: Array<ITrigger | IAction> = (isTrigger ? app?.triggers : app?.actions) || [];
|
||||
const actionsOrTriggers: Array<ITrigger | IAction> =
|
||||
(isTrigger ? app?.triggers : app?.actions) || [];
|
||||
const substeps = React.useMemo(
|
||||
() =>
|
||||
actionsOrTriggers?.find(({ key }: ITrigger | IAction) => key === step.key)?.substeps || [],
|
||||
actionsOrTriggers?.find(({ key }: ITrigger | IAction) => key === step.key)
|
||||
?.substeps || [],
|
||||
[actionsOrTriggers, step?.key]
|
||||
);
|
||||
|
||||
@@ -159,7 +177,10 @@ export default function FlowStep(
|
||||
handleChange({ step: val as IStep });
|
||||
};
|
||||
|
||||
const stepValidationSchema = React.useMemo(() => generateValidationSchema(substeps), [substeps]);
|
||||
const stepValidationSchema = React.useMemo(
|
||||
() => generateValidationSchema(substeps),
|
||||
[substeps]
|
||||
);
|
||||
|
||||
if (!apps) return null;
|
||||
|
||||
@@ -183,7 +204,11 @@ export default function FlowStep(
|
||||
step.status === 'completed' ? validIcon : errorIcon;
|
||||
|
||||
return (
|
||||
<Wrapper elevation={collapsed ? 1 : 4} onClick={onOpen} data-test="flow-step">
|
||||
<Wrapper
|
||||
elevation={collapsed ? 1 : 4}
|
||||
onClick={onOpen}
|
||||
data-test="flow-step"
|
||||
>
|
||||
<Header collapsed={collapsed}>
|
||||
<Stack direction="row" alignItems="center" gap={2}>
|
||||
<AppIconWrapper>
|
||||
@@ -236,7 +261,11 @@ export default function FlowStep(
|
||||
>
|
||||
<ChooseAppAndEventSubstep
|
||||
expanded={currentSubstep === 0}
|
||||
substep={{ key: 'chooAppAndEvent', name: 'Choose app & event', arguments: [] }}
|
||||
substep={{
|
||||
key: 'chooAppAndEvent',
|
||||
name: 'Choose app & event',
|
||||
arguments: [],
|
||||
}}
|
||||
onExpand={() => toggleSubstep(0)}
|
||||
onCollapse={() => toggleSubstep(0)}
|
||||
onSubmit={expandNextStep}
|
||||
@@ -245,40 +274,38 @@ export default function FlowStep(
|
||||
/>
|
||||
|
||||
{substeps?.length > 0 &&
|
||||
substeps.map(
|
||||
(
|
||||
substep: ISubstep,
|
||||
index: number
|
||||
) => (
|
||||
<React.Fragment key={`${substep?.name}-${index}`}>
|
||||
{substep.key === 'chooseConnection' && app && (
|
||||
<ChooseConnectionSubstep
|
||||
expanded={currentSubstep === index + 1}
|
||||
substep={substep}
|
||||
onExpand={() => toggleSubstep(index + 1)}
|
||||
onCollapse={() => toggleSubstep(index + 1)}
|
||||
onSubmit={expandNextStep}
|
||||
onChange={handleChange}
|
||||
application={app}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
substeps.map((substep: ISubstep, index: number) => (
|
||||
<React.Fragment key={`${substep?.name}-${index}`}>
|
||||
{substep.key === 'chooseConnection' && app && (
|
||||
<ChooseConnectionSubstep
|
||||
expanded={currentSubstep === index + 1}
|
||||
substep={substep}
|
||||
onExpand={() => toggleSubstep(index + 1)}
|
||||
onCollapse={() => toggleSubstep(index + 1)}
|
||||
onSubmit={expandNextStep}
|
||||
onChange={handleChange}
|
||||
application={app}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
|
||||
{substep.key === 'testStep' && (
|
||||
<TestSubstep
|
||||
expanded={currentSubstep === index + 1}
|
||||
substep={substep}
|
||||
onExpand={() => toggleSubstep(index + 1)}
|
||||
onCollapse={() => toggleSubstep(index + 1)}
|
||||
onSubmit={expandNextStep}
|
||||
onChange={handleChange}
|
||||
onContinue={onContinue}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
{substep.key === 'testStep' && (
|
||||
<TestSubstep
|
||||
expanded={currentSubstep === index + 1}
|
||||
substep={substep}
|
||||
onExpand={() => toggleSubstep(index + 1)}
|
||||
onCollapse={() => toggleSubstep(index + 1)}
|
||||
onSubmit={expandNextStep}
|
||||
onChange={handleChange}
|
||||
onContinue={onContinue}
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
|
||||
{substep.key && ['chooseConnection', 'testStep'].includes(substep.key) ===
|
||||
false && (
|
||||
{substep.key &&
|
||||
['chooseConnection', 'testStep'].includes(
|
||||
substep.key
|
||||
) === false && (
|
||||
<FlowSubstep
|
||||
expanded={currentSubstep === index + 1}
|
||||
substep={substep}
|
||||
@@ -289,9 +316,8 @@ export default function FlowStep(
|
||||
step={step}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Form>
|
||||
</StepExecutionsProvider>
|
||||
</List>
|
||||
|
@@ -27,15 +27,17 @@ export const Wrapper = styled(Card)`
|
||||
|
||||
type HeaderProps = {
|
||||
collapsed?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const Header = styled('div', { shouldForwardProp: prop => prop !== 'collapsed' })<HeaderProps>`
|
||||
export const Header = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'collapsed',
|
||||
})<HeaderProps>`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
cursor: ${({ collapsed }) => collapsed ? 'pointer' : 'unset'};
|
||||
cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')};
|
||||
`;
|
||||
|
||||
export const Content = styled('div')`
|
||||
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, .8)};
|
||||
border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)};
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
padding: ${({ theme }) => theme.spacing(2, 0)};
|
||||
|
@@ -14,19 +14,22 @@ type FlowStepContextMenuProps = {
|
||||
deletable: boolean;
|
||||
};
|
||||
|
||||
function FlowStepContextMenu(props: FlowStepContextMenuProps): React.ReactElement {
|
||||
function FlowStepContextMenu(
|
||||
props: FlowStepContextMenuProps
|
||||
): React.ReactElement {
|
||||
const { stepId, onClose, anchorEl, deletable } = props;
|
||||
const [deleteStep] = useMutation(DELETE_STEP, {
|
||||
refetchQueries: [
|
||||
'GetFlow'
|
||||
]
|
||||
refetchQueries: ['GetFlow'],
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
const deleteActionHandler = React.useCallback(async (event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
await deleteStep({ variables: { input: { id: stepId } } });
|
||||
}, [stepId]);
|
||||
const deleteActionHandler = React.useCallback(
|
||||
async (event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
await deleteStep({ variables: { input: { id: stepId } } });
|
||||
},
|
||||
[stepId]
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
@@ -35,11 +38,13 @@ function FlowStepContextMenu(props: FlowStepContextMenuProps): React.ReactElemen
|
||||
hideBackdrop={false}
|
||||
anchorEl={anchorEl}
|
||||
>
|
||||
{deletable && <MenuItem onClick={deleteActionHandler}>
|
||||
{formatMessage('connection.delete')}
|
||||
</MenuItem>}
|
||||
{deletable && (
|
||||
<MenuItem onClick={deleteActionHandler}>
|
||||
{formatMessage('connection.delete')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default FlowStepContextMenu;
|
||||
|
@@ -11,7 +11,7 @@ import InputCreator from 'components/InputCreator';
|
||||
import type { IField, IStep, ISubstep } from '@automatisch/types';
|
||||
|
||||
type FlowSubstepProps = {
|
||||
substep: ISubstep,
|
||||
substep: ISubstep;
|
||||
expanded?: boolean;
|
||||
onExpand: () => void;
|
||||
onCollapse: () => void;
|
||||
@@ -25,8 +25,10 @@ const validateSubstep = (substep: ISubstep, step: IStep) => {
|
||||
|
||||
const args: IField[] = substep.arguments || [];
|
||||
|
||||
return args.every(arg => {
|
||||
if (arg.required === false) { return true; }
|
||||
return args.every((arg) => {
|
||||
if (arg.required === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const argValue = step.parameters?.[arg.key];
|
||||
|
||||
@@ -47,20 +49,19 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
||||
step,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
name,
|
||||
arguments: args,
|
||||
} = substep;
|
||||
const { name, arguments: args } = substep;
|
||||
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
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));
|
||||
|
||||
React.useEffect(() => {
|
||||
function validate (step: unknown) {
|
||||
function validate(step: unknown) {
|
||||
const validationResult = validateSubstep(substep, step as IStep);
|
||||
setValidationStatus(validationResult);
|
||||
};
|
||||
}
|
||||
const subscription = formContext.watch(validate);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
@@ -77,11 +78,15 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
||||
valid={validationStatus}
|
||||
/>
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Stack
|
||||
width='100%'
|
||||
spacing={2}
|
||||
>
|
||||
<ListItem
|
||||
sx={{
|
||||
pt: 2,
|
||||
pb: 3,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Stack width="100%" spacing={2}>
|
||||
{args?.map((argument) => (
|
||||
<InputCreator
|
||||
key={argument.key}
|
||||
@@ -108,6 +113,6 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default FlowSubstep;
|
||||
|
@@ -4,7 +4,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
|
||||
import { ListItemButton, Typography} from './style';
|
||||
import { ListItemButton, Typography } from './style';
|
||||
|
||||
type FlowSubstepTitleProps = {
|
||||
expanded?: boolean;
|
||||
@@ -17,12 +17,7 @@ const validIcon = <CheckCircleIcon color="success" />;
|
||||
const errorIcon = <ErrorIcon color="error" />;
|
||||
|
||||
function FlowSubstepTitle(props: FlowSubstepTitleProps): React.ReactElement {
|
||||
const {
|
||||
expanded = false,
|
||||
onClick = () => null,
|
||||
valid = null,
|
||||
title,
|
||||
} = props;
|
||||
const { expanded = false, onClick = () => null, valid = null, title } = props;
|
||||
|
||||
const hasValidation = valid !== null;
|
||||
const validationStatusIcon = valid ? validIcon : errorIcon;
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { FormProvider, useForm, useWatch, FieldValues, SubmitHandler, UseFormReturn } from 'react-hook-form';
|
||||
import {
|
||||
FormProvider,
|
||||
useForm,
|
||||
useWatch,
|
||||
FieldValues,
|
||||
SubmitHandler,
|
||||
UseFormReturn,
|
||||
} from 'react-hook-form';
|
||||
import type { UseFormProps } from 'react-hook-form';
|
||||
|
||||
type FormProps = {
|
||||
@@ -7,8 +14,8 @@ type FormProps = {
|
||||
defaultValues?: UseFormProps['defaultValues'];
|
||||
onSubmit?: SubmitHandler<FieldValues>;
|
||||
render?: (props: UseFormReturn) => React.ReactNode;
|
||||
resolver?: UseFormProps["resolver"];
|
||||
mode?: UseFormProps["mode"];
|
||||
resolver?: UseFormProps['resolver'];
|
||||
mode?: UseFormProps['mode'];
|
||||
};
|
||||
|
||||
const noop = () => null;
|
||||
@@ -31,7 +38,7 @@ export default function Form(props: FormProps): React.ReactElement {
|
||||
mode,
|
||||
});
|
||||
|
||||
const form = useWatch({ control: methods.control, });
|
||||
const form = useWatch({ control: methods.control });
|
||||
|
||||
/**
|
||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||
@@ -51,4 +58,4 @@ export default function Form(props: FormProps): React.ReactElement {
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -5,7 +5,5 @@ import useScrollTrigger from '@mui/material/useScrollTrigger';
|
||||
export default function HideOnScroll(props: SlideProps): React.ReactElement {
|
||||
const trigger = useScrollTrigger();
|
||||
|
||||
return (
|
||||
<Slide appear={false} direction="down" in={!trigger} {...props} />
|
||||
);
|
||||
};
|
||||
return <Slide appear={false} direction="down" in={!trigger} {...props} />;
|
||||
}
|
||||
|
@@ -21,17 +21,13 @@ type RawOption = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const optionGenerator = (options: RawOption[]): IFieldDropdownOption[] => options?.map(({ name, value }) => ({ label: name as string, value: value }));
|
||||
const optionGenerator = (options: RawOption[]): IFieldDropdownOption[] =>
|
||||
options?.map(({ name, value }) => ({ label: name as string, value: value }));
|
||||
|
||||
export default function InputCreator(props: InputCreatorProps): React.ReactElement {
|
||||
const {
|
||||
onChange,
|
||||
onBlur,
|
||||
schema,
|
||||
namePrefix,
|
||||
stepId,
|
||||
disabled,
|
||||
} = props;
|
||||
export default function InputCreator(
|
||||
props: InputCreatorProps
|
||||
): React.ReactElement {
|
||||
const { onChange, onBlur, schema, namePrefix, stepId, disabled } = props;
|
||||
|
||||
const {
|
||||
key: name,
|
||||
@@ -101,5 +97,5 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
||||
);
|
||||
}
|
||||
|
||||
return (<React.Fragment />)
|
||||
};
|
||||
return <React.Fragment />;
|
||||
}
|
||||
|
@@ -5,16 +5,18 @@ import { Container } from './style';
|
||||
|
||||
type IntermediateStepCountProps = {
|
||||
count: number;
|
||||
}
|
||||
};
|
||||
|
||||
export default function IntermediateStepCount(props: IntermediateStepCountProps) {
|
||||
export default function IntermediateStepCount(
|
||||
props: IntermediateStepCountProps
|
||||
) {
|
||||
const { count } = props;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="subtitle1" sx={{ }}>
|
||||
<Typography variant="subtitle1" sx={{}}>
|
||||
+{count}
|
||||
</Typography>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,11 @@ type IntlProviderProps = {
|
||||
|
||||
const IntlProvider = ({ children }: IntlProviderProps): React.ReactElement => {
|
||||
return (
|
||||
<BaseIntlProvider locale={navigator.language} defaultLocale="en" messages={englishMessages}>
|
||||
<BaseIntlProvider
|
||||
locale={navigator.language}
|
||||
defaultLocale="en"
|
||||
messages={englishMessages}
|
||||
>
|
||||
{children}
|
||||
</BaseIntlProvider>
|
||||
);
|
||||
|
@@ -4,7 +4,7 @@ import type { IJSONObject } from '@automatisch/types';
|
||||
|
||||
type JSONViewerProps = {
|
||||
data: IJSONObject;
|
||||
}
|
||||
};
|
||||
|
||||
const theme = {
|
||||
scheme: 'inspector',
|
||||
|
@@ -1,29 +1,35 @@
|
||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||
|
||||
export const jsonViewerStyles = (<GlobalStyles styles={(theme) => ({
|
||||
'json-viewer': {
|
||||
'--background-color': 'transparent',
|
||||
'--font-family': 'monaco, Consolas, Lucida Console, monospace',
|
||||
'--font-size': '1rem',
|
||||
'--indent-size': '1.5em',
|
||||
'--indentguide-size': '1px',
|
||||
'--indentguide-style': 'solid',
|
||||
'--indentguide-color': theme.palette.text.primary,
|
||||
'--indentguide-color-active': '#666',
|
||||
'--indentguide': 'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color)',
|
||||
'--indentguide-active': 'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color-active)',
|
||||
export const jsonViewerStyles = (
|
||||
<GlobalStyles
|
||||
styles={(theme) => ({
|
||||
'json-viewer': {
|
||||
'--background-color': 'transparent',
|
||||
'--font-family': 'monaco, Consolas, Lucida Console, monospace',
|
||||
'--font-size': '1rem',
|
||||
'--indent-size': '1.5em',
|
||||
'--indentguide-size': '1px',
|
||||
'--indentguide-style': 'solid',
|
||||
'--indentguide-color': theme.palette.text.primary,
|
||||
'--indentguide-color-active': '#666',
|
||||
'--indentguide':
|
||||
'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color)',
|
||||
'--indentguide-active':
|
||||
'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color-active)',
|
||||
|
||||
/* Types colors */
|
||||
'--string-color': theme.palette.text.secondary,
|
||||
'--number-color': theme.palette.text.primary,
|
||||
'--boolean-color': theme.palette.text.primary,
|
||||
'--null-color': theme.palette.text.primary,
|
||||
'--property-color': theme.palette.text.primary,
|
||||
/* Types colors */
|
||||
'--string-color': theme.palette.text.secondary,
|
||||
'--number-color': theme.palette.text.primary,
|
||||
'--boolean-color': theme.palette.text.primary,
|
||||
'--null-color': theme.palette.text.primary,
|
||||
'--property-color': theme.palette.text.primary,
|
||||
|
||||
/* Collapsed node preview */
|
||||
'--preview-color': theme.palette.text.primary,
|
||||
/* Collapsed node preview */
|
||||
'--preview-color': theme.palette.text.primary,
|
||||
|
||||
/* Search highlight color */
|
||||
'--highlight-color': '#6fb3d2',
|
||||
}
|
||||
})} />)
|
||||
/* Search highlight color */
|
||||
'--highlight-color': '#6fb3d2',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
@@ -15,7 +15,7 @@ import Drawer from 'components/Drawer';
|
||||
|
||||
type PublicLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
const drawerLinks = [
|
||||
{
|
||||
@@ -45,12 +45,16 @@ const generateDrawerBottomLinks = ({ notificationBadgeContent = 0 }) => [
|
||||
to: URLS.UPDATES,
|
||||
badgeContent: notificationBadgeContent,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
export default function PublicLayout({ children }: PublicLayoutProps): React.ReactElement {
|
||||
export default function PublicLayout({
|
||||
children,
|
||||
}: PublicLayoutProps): React.ReactElement {
|
||||
const version = useVersion();
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), { noSsr: true });
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), {
|
||||
noSsr: true,
|
||||
});
|
||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
@@ -62,9 +66,13 @@ export default function PublicLayout({ children }: PublicLayoutProps): React.Rea
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar drawerOpen={isDrawerOpen} onDrawerOpen={openDrawer} onDrawerClose={closeDrawer} />
|
||||
<AppBar
|
||||
drawerOpen={isDrawerOpen}
|
||||
onDrawerOpen={openDrawer}
|
||||
onDrawerClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', }}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Drawer
|
||||
links={drawerLinks}
|
||||
bottomLinks={drawerBottomLinks}
|
||||
@@ -73,7 +81,7 @@ export default function PublicLayout({ children }: PublicLayoutProps): React.Rea
|
||||
onClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ flex: 1, }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Toolbar />
|
||||
|
||||
{children}
|
||||
|
@@ -11,21 +11,22 @@ type ListItemLinkProps = {
|
||||
to: string;
|
||||
onClick?: (event: React.SyntheticEvent) => void;
|
||||
'data-test'?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function ListItemLink(props: ListItemLinkProps): React.ReactElement {
|
||||
export default function ListItemLink(
|
||||
props: ListItemLinkProps
|
||||
): React.ReactElement {
|
||||
const { icon, primary, to, onClick, 'data-test': dataTest } = props;
|
||||
const selected = useMatch({ path: to, end: true });
|
||||
|
||||
const CustomLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InLineLink(
|
||||
linkProps,
|
||||
ref,
|
||||
) {
|
||||
return <Link ref={ref} to={to} {...linkProps} />;
|
||||
}),
|
||||
[to],
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
||||
function InLineLink(linkProps, ref) {
|
||||
return <Link ref={ref} to={to} {...linkProps} />;
|
||||
}
|
||||
),
|
||||
[to]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -38,8 +39,11 @@ export default function ListItemLink(props: ListItemLinkProps): React.ReactEleme
|
||||
data-test={dataTest}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 52 }}>{icon}</ListItemIcon>
|
||||
<ListItemText primary={primary} primaryTypographyProps={{ variant: 'body1', }} />
|
||||
<ListItemText
|
||||
primary={primary}
|
||||
primaryTypographyProps={{ variant: 'body1' }}
|
||||
/>
|
||||
</ListItem>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ import { LOGIN } from 'graphql/mutations/login';
|
||||
import Form from 'components/Form';
|
||||
import TextField from 'components/TextField';
|
||||
|
||||
|
||||
function renderFields(props: { loading: boolean }) {
|
||||
const { loading = false } = props;
|
||||
|
||||
@@ -52,7 +51,7 @@ function renderFields(props: { loading: boolean }) {
|
||||
</LoadingButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function LoginForm() {
|
||||
@@ -69,7 +68,7 @@ function LoginForm() {
|
||||
const handleSubmit = async (values: any) => {
|
||||
const { data } = await login({
|
||||
variables: {
|
||||
input: values
|
||||
input: values,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -85,14 +84,20 @@ function LoginForm() {
|
||||
<Typography
|
||||
variant="h3"
|
||||
align="center"
|
||||
sx={{ borderBottom: '1px solid', borderColor: (theme) => theme.palette.text.disabled, pb: 2, mb: 2 }}
|
||||
gutterBottom>
|
||||
sx={{
|
||||
borderBottom: '1px solid',
|
||||
borderColor: (theme) => theme.palette.text.disabled,
|
||||
pb: 2,
|
||||
mb: 2,
|
||||
}}
|
||||
gutterBottom
|
||||
>
|
||||
Login
|
||||
</Typography>
|
||||
|
||||
<Form onSubmit={handleSubmit} render={render} />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default LoginForm;
|
||||
|
@@ -10,22 +10,23 @@ import { CardContent } from './style';
|
||||
type NoResultFoundProps = {
|
||||
text?: string;
|
||||
to?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export default function NoResultFound(props: NoResultFoundProps): React.ReactElement {
|
||||
export default function NoResultFound(
|
||||
props: NoResultFoundProps
|
||||
): React.ReactElement {
|
||||
const { text, to } = props;
|
||||
|
||||
const ActionAreaLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
|
||||
linkProps,
|
||||
ref,
|
||||
) {
|
||||
if (!to) return <div>{linkProps.children}</div>;
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
||||
function InlineLink(linkProps, ref) {
|
||||
if (!to) return <div>{linkProps.children}</div>;
|
||||
|
||||
return <Link ref={ref} to={to} {...linkProps} />;
|
||||
}),
|
||||
[to],
|
||||
return <Link ref={ref} to={to} {...linkProps} />;
|
||||
}
|
||||
),
|
||||
[to]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -34,11 +35,9 @@ export default function NoResultFound(props: NoResultFoundProps): React.ReactEle
|
||||
<CardContent>
|
||||
{!!to && <AddCircleIcon color="primary" />}
|
||||
|
||||
<Typography variant="body1">
|
||||
{text}
|
||||
</Typography>
|
||||
<Typography variant="body1">{text}</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
)
|
||||
};
|
||||
);
|
||||
}
|
||||
|
@@ -8,4 +8,4 @@ export const CardContent = styled(MuiCardContent)`
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
min-height: 200px;
|
||||
`;
|
||||
`;
|
||||
|
@@ -15,32 +15,26 @@ interface NotificationCardProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
const getHumanlyDate = (timestamp: number) => DateTime.fromMillis(timestamp).toRelative();
|
||||
const getHumanlyDate = (timestamp: number) =>
|
||||
DateTime.fromMillis(timestamp).toRelative();
|
||||
|
||||
export default function NotificationCard(props: NotificationCardProps) {
|
||||
const {
|
||||
name,
|
||||
createdAt,
|
||||
documentationUrl,
|
||||
description,
|
||||
} = props;
|
||||
const { name, createdAt, documentationUrl, description } = props;
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const relativeCreatedAt = getHumanlyDate((new Date(createdAt)).getTime());
|
||||
const subheader = formatMessage('notification.releasedAt', { relativeDate: relativeCreatedAt });
|
||||
const relativeCreatedAt = getHumanlyDate(new Date(createdAt).getTime());
|
||||
const subheader = formatMessage('notification.releasedAt', {
|
||||
relativeDate: relativeCreatedAt,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardActionArea
|
||||
component={'a'}
|
||||
href={documentationUrl}
|
||||
target="_blank"
|
||||
>
|
||||
<CardActionArea component={'a'} href={documentationUrl} target="_blank">
|
||||
<CardHeader
|
||||
title={name}
|
||||
titleTypographyProps={{ variant: 'h6' }}
|
||||
subheader={subheader}
|
||||
sx={{ borderBottom: '1px solid', borderColor: 'divider'}}
|
||||
sx={{ borderBottom: '1px solid', borderColor: 'divider' }}
|
||||
/>
|
||||
|
||||
<CardContent>
|
||||
|
@@ -4,7 +4,5 @@ import Typography, { TypographyProps } from '@mui/material/Typography';
|
||||
type PageTitleProps = TypographyProps;
|
||||
|
||||
export default function PageTitle(props: PageTitleProps): React.ReactElement {
|
||||
return (
|
||||
<Typography variant="h3" {...props} />
|
||||
);
|
||||
}
|
||||
return <Typography variant="h3" {...props} />;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import * as React from 'react'
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
type PortalProps = {
|
||||
@@ -8,7 +8,7 @@ type PortalProps = {
|
||||
const Portal = ({ children }: PortalProps) => {
|
||||
return typeof document === 'object'
|
||||
? ReactDOM.createPortal(children, document.body)
|
||||
: null
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
export default Portal;
|
||||
|
@@ -6,12 +6,7 @@ import InputLabel from '@mui/material/InputLabel';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { Editor, Transforms, Range, createEditor } from 'slate';
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
useSelected,
|
||||
useFocused,
|
||||
} from 'slate-react';
|
||||
import { Slate, Editable, useSelected, useFocused } from 'slate-react';
|
||||
|
||||
import {
|
||||
serialize,
|
||||
@@ -39,7 +34,7 @@ type PowerInputProps = {
|
||||
docUrl?: string;
|
||||
clickToCopy?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const PowerInput = (props: PowerInputProps) => {
|
||||
const { control } = useFormContext();
|
||||
@@ -54,21 +49,28 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
} = props;
|
||||
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const renderElement = React.useCallback(props => <Element {...props} />, []);
|
||||
const renderElement = React.useCallback(
|
||||
(props) => <Element {...props} />,
|
||||
[]
|
||||
);
|
||||
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
||||
const [showVariableSuggestions, setShowVariableSuggestions] = React.useState(false);
|
||||
const [showVariableSuggestions, setShowVariableSuggestions] =
|
||||
React.useState(false);
|
||||
|
||||
const stepsWithVariables = React.useMemo(() => {
|
||||
return processStepWithExecutions(priorStepsWithExecutions);
|
||||
}, [priorStepsWithExecutions])
|
||||
}, [priorStepsWithExecutions]);
|
||||
|
||||
const handleBlur = React.useCallback((value) => {
|
||||
onBlur?.(value);
|
||||
}, [onBlur]);
|
||||
const handleBlur = React.useCallback(
|
||||
(value) => {
|
||||
onBlur?.(value);
|
||||
},
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
const handleVariableSuggestionClick = React.useCallback(
|
||||
(variable: Pick<VariableElement, "name" | "value">) => {
|
||||
insertVariable(editor, variable, stepsWithVariables);
|
||||
(variable: Pick<VariableElement, 'name' | 'value'>) => {
|
||||
insertVariable(editor, variable, stepsWithVariables);
|
||||
},
|
||||
[stepsWithVariables]
|
||||
);
|
||||
@@ -80,17 +82,25 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
shouldUnregister={false}
|
||||
render={({ field: { value, onChange: controllerOnChange, onBlur: controllerOnBlur, } }) => (
|
||||
render={({
|
||||
field: {
|
||||
value,
|
||||
onChange: controllerOnChange,
|
||||
onBlur: controllerOnBlur,
|
||||
},
|
||||
}) => (
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={deserialize(value, stepsWithVariables)}
|
||||
onChange={value => {
|
||||
onChange={(value) => {
|
||||
controllerOnChange(serialize(value));
|
||||
}}
|
||||
>
|
||||
<ClickAwayListener
|
||||
mouseEvent="onMouseDown"
|
||||
onClickAway={() => { setShowVariableSuggestions(false); }}
|
||||
onClickAway={() => {
|
||||
setShowVariableSuggestions(false);
|
||||
}}
|
||||
>
|
||||
{/* ref-able single child for ClickAwayListener */}
|
||||
<div style={{ width: '100%' }} data-test="power-input">
|
||||
@@ -100,7 +110,7 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
shrink={true}
|
||||
disabled={disabled}
|
||||
variant="outlined"
|
||||
sx={{ bgcolor: 'white', display: 'inline-block', px: .75 }}
|
||||
sx={{ bgcolor: 'white', display: 'inline-block', px: 0.75 }}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
@@ -113,17 +123,16 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
onFocus={() => {
|
||||
setShowVariableSuggestions(true);
|
||||
}}
|
||||
onBlur={() => { controllerOnBlur(); handleBlur(value); }}
|
||||
onBlur={() => {
|
||||
controllerOnBlur();
|
||||
handleBlur(value);
|
||||
}}
|
||||
/>
|
||||
</FakeInput>
|
||||
{/* ghost placer for the variables popover */}
|
||||
<div ref={editorRef} style={{ width: '100%' }} />
|
||||
|
||||
<FormHelperText
|
||||
variant="outlined"
|
||||
>
|
||||
{description}
|
||||
</FormHelperText>
|
||||
<FormHelperText variant="outlined">{description}</FormHelperText>
|
||||
|
||||
<SuggestionsPopper
|
||||
open={showVariableSuggestions}
|
||||
@@ -136,36 +145,28 @@ const PowerInput = (props: PowerInputProps) => {
|
||||
</Slate>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const SuggestionsPopper = (props: any) => {
|
||||
const {
|
||||
open,
|
||||
anchorEl,
|
||||
data,
|
||||
onSuggestionClick,
|
||||
} = props;
|
||||
const { open, anchorEl, data, onSuggestionClick } = props;
|
||||
|
||||
return (
|
||||
<Popper
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
style={{ width: anchorEl?.clientWidth, zIndex: 1, }}
|
||||
style={{ width: anchorEl?.clientWidth, zIndex: 1 }}
|
||||
modifiers={[
|
||||
{
|
||||
name: 'flip',
|
||||
enabled: false,
|
||||
options: {
|
||||
altBoundary: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Suggestions
|
||||
data={data}
|
||||
onSuggestionClick={onSuggestionClick}
|
||||
/>
|
||||
<Suggestions data={data} onSuggestionClick={onSuggestionClick} />
|
||||
</Popper>
|
||||
);
|
||||
};
|
||||
@@ -178,9 +179,9 @@ const Element = (props: any) => {
|
||||
default:
|
||||
return <p {...attributes}>{children}</p>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Variable = ({ attributes, children, element }: any) => {
|
||||
const Variable = ({ attributes, children, element }: any) => {
|
||||
const selected = useSelected();
|
||||
const focused = useFocused();
|
||||
const label = (
|
||||
@@ -200,7 +201,7 @@ const Variable = ({ attributes, children, element }: any) => {
|
||||
size="small"
|
||||
label={label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PowerInput;
|
||||
|
@@ -7,17 +7,21 @@ export const InputLabelWrapper = styled('div')`
|
||||
left: -6px;
|
||||
`;
|
||||
|
||||
export const FakeInput = styled('div', { shouldForwardProp: prop => prop !== 'disabled'})<{ disabled?: boolean }>`
|
||||
export const FakeInput = styled('div', {
|
||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||
})<{ disabled?: boolean }>`
|
||||
border: 1px solid #eee;
|
||||
min-height: 52px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: ${({ theme }) => theme.spacing(0, 1.75)};
|
||||
border-radius: ${({ theme }) => theme.spacing(.5)};
|
||||
border-radius: ${({ theme }) => theme.spacing(0.5)};
|
||||
border-color: rgba(0, 0, 0, 0.23);
|
||||
position: relative;
|
||||
|
||||
${({ disabled, theme }) => !!disabled && `
|
||||
${({ disabled, theme }) =>
|
||||
!!disabled &&
|
||||
`
|
||||
color: ${theme.palette.action.disabled},
|
||||
border-color: ${theme.palette.action.disabled},
|
||||
`}
|
||||
|
@@ -6,7 +6,7 @@ export type VariableElement = {
|
||||
value?: unknown;
|
||||
name?: string;
|
||||
children: Text[];
|
||||
}
|
||||
};
|
||||
|
||||
export type ParagraphElement = {
|
||||
type: 'paragraph';
|
||||
|
@@ -2,13 +2,12 @@ import { Text, Descendant, Transforms } from 'slate';
|
||||
import { withHistory } from 'slate-history';
|
||||
import { withReact } from 'slate-react';
|
||||
|
||||
import type {
|
||||
CustomEditor,
|
||||
CustomElement,
|
||||
VariableElement,
|
||||
} from './types';
|
||||
import type { CustomEditor, CustomElement, VariableElement } from './types';
|
||||
|
||||
function getStepPosition(id: string, stepsWithVariables: Record<string, unknown>[]) {
|
||||
function getStepPosition(
|
||||
id: string,
|
||||
stepsWithVariables: Record<string, unknown>[]
|
||||
) {
|
||||
const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => {
|
||||
return stepWithVariables.id === id;
|
||||
});
|
||||
@@ -16,30 +15,42 @@ function getStepPosition(id: string, stepsWithVariables: Record<string, unknown>
|
||||
return stepIndex + 1;
|
||||
}
|
||||
|
||||
function humanizeVariableName(variableName: string, stepsWithVariables: Record<string, unknown>[]) {
|
||||
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}.`);
|
||||
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: '', }],
|
||||
}];
|
||||
export const deserialize = (
|
||||
value: string,
|
||||
stepsWithVariables: any[]
|
||||
): Descendant[] => {
|
||||
if (!value)
|
||||
return [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }],
|
||||
},
|
||||
];
|
||||
|
||||
return value.split('\n').map(line => {
|
||||
return value.split('\n').map((line) => {
|
||||
const nodes = line.split(variableRegExp);
|
||||
|
||||
if (nodes.length > 1) {
|
||||
return {
|
||||
type: 'paragraph',
|
||||
children: nodes.map(node => {
|
||||
children: nodes.map((node) => {
|
||||
if (node.match(variableRegExp)) {
|
||||
return {
|
||||
type: 'variable',
|
||||
@@ -52,19 +63,19 @@ export const deserialize = (value: string, stepsWithVariables: any[]): Descendan
|
||||
return {
|
||||
text: node,
|
||||
};
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'paragraph',
|
||||
children: [{ text: line }],
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const serialize = (value: Descendant[]): string => {
|
||||
return value.map(node => serializeNode(node)).join('\n');
|
||||
return value.map((node) => serializeNode(node)).join('\n');
|
||||
};
|
||||
|
||||
const serializeNode = (node: CustomElement | Descendant): string => {
|
||||
@@ -76,7 +87,7 @@ const serializeNode = (node: CustomElement | Descendant): string => {
|
||||
return node.value as string;
|
||||
}
|
||||
|
||||
return node.children.map(n => serializeNode(n)).join('');
|
||||
return node.children.map((n) => serializeNode(n)).join('');
|
||||
};
|
||||
|
||||
export const withVariables = (editor: CustomEditor) => {
|
||||
@@ -84,16 +95,20 @@ export const withVariables = (editor: CustomEditor) => {
|
||||
|
||||
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>[]) => {
|
||||
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),
|
||||
@@ -103,7 +118,7 @@ export const insertVariable = (editor: CustomEditor, variableData: Pick<Variable
|
||||
|
||||
Transforms.insertNodes(editor, variable);
|
||||
Transforms.move(editor);
|
||||
}
|
||||
};
|
||||
|
||||
export const customizeEditor = (editor: CustomEditor): CustomEditor => {
|
||||
return withVariables(withReact(withHistory(editor)));
|
||||
|
@@ -10,10 +10,9 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar>
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
} from 'react-router-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
export default Router;
|
||||
|
@@ -11,14 +11,14 @@ type SearchInputProps = {
|
||||
onChange?: (event: React.ChangeEvent) => void;
|
||||
};
|
||||
|
||||
export default function SearchInput({ onChange }: SearchInputProps): React.ReactElement {
|
||||
export default function SearchInput({
|
||||
onChange,
|
||||
}: SearchInputProps): React.ReactElement {
|
||||
const formatMessage = useFormatMessage();
|
||||
|
||||
return (
|
||||
<FormControl variant="outlined" fullWidth>
|
||||
<InputLabel
|
||||
htmlFor="search-input"
|
||||
>
|
||||
<InputLabel htmlFor="search-input">
|
||||
{formatMessage('searchPlaceholder')}
|
||||
</InputLabel>
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import Drawer from 'components/Drawer';
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
const drawerLinks = [
|
||||
{
|
||||
@@ -27,12 +27,16 @@ const drawerBottomLinks = [
|
||||
Icon: ArrowBackIosNewIcon,
|
||||
primary: 'settingsDrawer.goBack',
|
||||
to: '/',
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsLayout({ children }: SettingsLayoutProps): React.ReactElement {
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: SettingsLayoutProps): React.ReactElement {
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), { noSsr: true });
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'), {
|
||||
noSsr: true,
|
||||
});
|
||||
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
|
||||
|
||||
const openDrawer = () => setDrawerOpen(true);
|
||||
@@ -40,9 +44,13 @@ export default function SettingsLayout({ children }: SettingsLayoutProps): React
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar drawerOpen={isDrawerOpen} onDrawerOpen={openDrawer} onDrawerClose={closeDrawer} />
|
||||
<AppBar
|
||||
drawerOpen={isDrawerOpen}
|
||||
onDrawerOpen={openDrawer}
|
||||
onDrawerClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', }}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Drawer
|
||||
links={drawerLinks}
|
||||
bottomLinks={drawerBottomLinks}
|
||||
@@ -51,7 +59,7 @@ export default function SettingsLayout({ children }: SettingsLayoutProps): React
|
||||
onClose={closeDrawer}
|
||||
/>
|
||||
|
||||
<Box sx={{ flex: 1, }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Toolbar />
|
||||
|
||||
{children}
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { SnackbarProvider as BaseSnackbarProvider, SnackbarProviderProps } from 'notistack';
|
||||
import {
|
||||
SnackbarProvider as BaseSnackbarProvider,
|
||||
SnackbarProviderProps,
|
||||
} from 'notistack';
|
||||
|
||||
const SnackbarProvider = (props: SnackbarProviderProps): React.ReactElement => {
|
||||
return (
|
||||
@@ -11,7 +14,7 @@ const SnackbarProvider = (props: SnackbarProviderProps): React.ReactElement => {
|
||||
}}
|
||||
dense
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default SnackbarProvider;
|
||||
|
@@ -10,12 +10,8 @@ export default function TabPanel(props: TabPanelProps): React.ReactElement {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
{...other}
|
||||
>
|
||||
<div role="tabpanel" hidden={value !== index} {...other}>
|
||||
{value === index && children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import FlowSubstepTitle from 'components/FlowSubstepTitle';
|
||||
import type { IStep, ISubstep } from '@automatisch/types';
|
||||
|
||||
type TestSubstepProps = {
|
||||
substep: ISubstep,
|
||||
substep: ISubstep;
|
||||
expanded?: boolean;
|
||||
onExpand: () => void;
|
||||
onCollapse: () => void;
|
||||
@@ -30,12 +30,16 @@ function serializeErrors(graphQLErrors: any) {
|
||||
try {
|
||||
return {
|
||||
...error,
|
||||
message: (<pre style={{ margin: 0 }}>{JSON.stringify(JSON.parse(error.message as string), null, 2)}</pre>),
|
||||
}
|
||||
message: (
|
||||
<pre style={{ margin: 0 }}>
|
||||
{JSON.stringify(JSON.parse(error.message as string), null, 2)}
|
||||
</pre>
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
return error;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function TestSubstep(props: TestSubstepProps): React.ReactElement {
|
||||
@@ -51,21 +55,25 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement {
|
||||
|
||||
const formatMessage = useFormatMessage();
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
const [executeFlow, { data, error, loading, called, reset }] = useMutation(EXECUTE_FLOW, { context: { autoSnackbar: false }});
|
||||
const [executeFlow, { data, error, loading, called, reset }] = useMutation(
|
||||
EXECUTE_FLOW,
|
||||
{ context: { autoSnackbar: false } }
|
||||
);
|
||||
const response = data?.executeFlow?.data;
|
||||
|
||||
const isCompleted = !error && called && !loading;
|
||||
const hasNoOutput = !response && isCompleted;
|
||||
|
||||
const {
|
||||
name,
|
||||
} = substep;
|
||||
const { name } = substep;
|
||||
|
||||
React.useEffect(function resetTestDataOnSubstepToggle() {
|
||||
if (!expanded) {
|
||||
reset();
|
||||
}
|
||||
}, [expanded, reset])
|
||||
React.useEffect(
|
||||
function resetTestDataOnSubstepToggle() {
|
||||
if (!expanded) {
|
||||
reset();
|
||||
}
|
||||
},
|
||||
[expanded, reset]
|
||||
);
|
||||
|
||||
const handleSubmit = React.useCallback(() => {
|
||||
if (isCompleted) {
|
||||
@@ -86,27 +94,44 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FlowSubstepTitle
|
||||
expanded={expanded}
|
||||
onClick={onToggle}
|
||||
title={name}
|
||||
/>
|
||||
<FlowSubstepTitle expanded={expanded} onClick={onToggle} title={name} />
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
{!!error?.graphQLErrors?.length && <Alert severity="error" sx={{ mb: 1, fontWeight: 500, width: '100%' }}>
|
||||
{serializeErrors(error.graphQLErrors).map((error: any) => (<div>{error.message}</div>))}
|
||||
</Alert>}
|
||||
<ListItem
|
||||
sx={{
|
||||
pt: 2,
|
||||
pb: 3,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{!!error?.graphQLErrors?.length && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{ mb: 1, fontWeight: 500, width: '100%' }}
|
||||
>
|
||||
{serializeErrors(error.graphQLErrors).map((error: any) => (
|
||||
<div>{error.message}</div>
|
||||
))}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{hasNoOutput && (
|
||||
<Alert severity="warning" sx={{ mb: 1, width: '100%' }}>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>{formatMessage('flowEditor.noTestDataTitle')}</AlertTitle>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
{formatMessage('flowEditor.noTestDataTitle')}
|
||||
</AlertTitle>
|
||||
|
||||
<Box sx={{ fontWeight: 400 }}>{formatMessage('flowEditor.noTestDataMessage')}</Box>
|
||||
<Box sx={{ fontWeight: 400 }}>
|
||||
{formatMessage('flowEditor.noTestDataMessage')}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{response && (
|
||||
<Box sx={{ maxHeight: 400, overflowY: 'auto', width: '100%' }} data-test="flow-test-substep-output">
|
||||
<Box
|
||||
sx={{ maxHeight: 400, overflowY: 'auto', width: '100%' }}
|
||||
data-test="flow-test-substep-output"
|
||||
>
|
||||
<JSONViewer data={response} />
|
||||
</Box>
|
||||
)}
|
||||
@@ -128,6 +153,6 @@ function TestSubstep(props: TestSubstepProps): React.ReactElement {
|
||||
</Collapse>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default TestSubstep;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import MuiTextField, { TextFieldProps as MuiTextFieldProps } from '@mui/material/TextField';
|
||||
import MuiTextField, {
|
||||
TextFieldProps as MuiTextFieldProps,
|
||||
} from '@mui/material/TextField';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
@@ -14,7 +16,9 @@ type TextFieldProps = {
|
||||
readOnly?: boolean;
|
||||
} & MuiTextFieldProps;
|
||||
|
||||
const createCopyAdornment = (ref: React.RefObject<HTMLInputElement | null>): React.ReactElement => {
|
||||
const createCopyAdornment = (
|
||||
ref: React.RefObject<HTMLInputElement | null>
|
||||
): React.ReactElement => {
|
||||
return (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
@@ -25,7 +29,7 @@ const createCopyAdornment = (ref: React.RefObject<HTMLInputElement | null>): Rea
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function TextField(props: TextFieldProps): React.ReactElement {
|
||||
const { control } = useFormContext();
|
||||
@@ -49,19 +53,38 @@ export default function TextField(props: TextFieldProps): React.ReactElement {
|
||||
defaultValue={defaultValue || ''}
|
||||
control={control}
|
||||
shouldUnregister={shouldUnregister}
|
||||
render={({ field: { ref, onChange: controllerOnChange, onBlur: controllerOnBlur, ...field } }) => (
|
||||
render={({
|
||||
field: {
|
||||
ref,
|
||||
onChange: controllerOnChange,
|
||||
onBlur: controllerOnBlur,
|
||||
...field
|
||||
},
|
||||
}) => (
|
||||
<MuiTextField
|
||||
{...textFieldProps}
|
||||
{...field}
|
||||
onChange={(...args) => { controllerOnChange(...args); onChange?.(...args); }}
|
||||
onBlur={(...args) => { controllerOnBlur(); onBlur?.(...args); }}
|
||||
inputRef={(element) => { inputRef.current = element; ref(element); }}
|
||||
InputProps={{ readOnly, endAdornment: clickToCopy ? createCopyAdornment(inputRef) : null}}
|
||||
onChange={(...args) => {
|
||||
controllerOnChange(...args);
|
||||
onChange?.(...args);
|
||||
}}
|
||||
onBlur={(...args) => {
|
||||
controllerOnBlur();
|
||||
onBlur?.(...args);
|
||||
}}
|
||||
inputRef={(element) => {
|
||||
inputRef.current = element;
|
||||
ref(element);
|
||||
}}
|
||||
InputProps={{
|
||||
readOnly,
|
||||
endAdornment: clickToCopy ? createCopyAdornment(inputRef) : null,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
TextField.defaultProps = {
|
||||
readOnly: false,
|
||||
|
@@ -7,7 +7,10 @@ type ThemeProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const ThemeProvider = ({ children, ...props }: ThemeProviderProps): React.ReactElement => {
|
||||
const ThemeProvider = ({
|
||||
children,
|
||||
...props
|
||||
}: ThemeProviderProps): React.ReactElement => {
|
||||
return (
|
||||
<BaseThemeProvider theme={theme} {...props}>
|
||||
<CssBaseline />
|
||||
|
@@ -1,5 +1,5 @@
|
||||
type Config = {
|
||||
[key: string]: string,
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
const config: Config = {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
export const CONNECTIONS = '/connections';
|
||||
export const EXECUTIONS = '/executions';
|
||||
export const EXECUTION_PATTERN = '/executions/:executionId';
|
||||
export const EXECUTION = (executionId: string): string => `/executions/${executionId}`;
|
||||
export const EXECUTION = (executionId: string): string =>
|
||||
`/executions/${executionId}`;
|
||||
|
||||
export const LOGIN = '/login';
|
||||
|
||||
@@ -9,21 +10,34 @@ export const APPS = '/apps';
|
||||
export const NEW_APP_CONNECTION = '/apps/new';
|
||||
export const APP = (appKey: string): string => `/app/${appKey}`;
|
||||
export const APP_PATTERN = '/app/:appKey';
|
||||
export const APP_CONNECTIONS = (appKey: string): string => `/app/${appKey}/connections`;
|
||||
export const APP_CONNECTIONS = (appKey: string): string =>
|
||||
`/app/${appKey}/connections`;
|
||||
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
|
||||
export const APP_ADD_CONNECTION = (appKey: string): string => `/app/${appKey}/connections/add`;
|
||||
export const APP_ADD_CONNECTION = (appKey: string): string =>
|
||||
`/app/${appKey}/connections/add`;
|
||||
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
|
||||
export const APP_RECONNECT_CONNECTION = (appKey: string, connectionId: string): string => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
export const APP_RECONNECT_CONNECTION_PATTERN = '/app/:appKey/connections/:connectionId/reconnect';
|
||||
export const APP_RECONNECT_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string
|
||||
): string => `/app/${appKey}/connections/${connectionId}/reconnect`;
|
||||
export const APP_RECONNECT_CONNECTION_PATTERN =
|
||||
'/app/:appKey/connections/:connectionId/reconnect';
|
||||
export const APP_FLOWS = (appKey: string): string => `/app/${appKey}/flows`;
|
||||
export const APP_FLOWS_FOR_CONNECTION = (appKey: string, connectionId: string): string => `/app/${appKey}/flows?connectionId=${connectionId}`;
|
||||
export const APP_FLOWS_FOR_CONNECTION = (
|
||||
appKey: string,
|
||||
connectionId: string
|
||||
): string => `/app/${appKey}/flows?connectionId=${connectionId}`;
|
||||
export const APP_FLOWS_PATTERN = '/app/:appKey/flows';
|
||||
|
||||
export const EDITOR = '/editor';
|
||||
export const CREATE_FLOW ='/editor/create';
|
||||
export const CREATE_FLOW_WITH_APP = (appKey: string) => `/editor/create?appKey=${appKey}`;
|
||||
export const CREATE_FLOW_WITH_APP_AND_CONNECTION = (appKey?: string, connectionId?: string) => {
|
||||
const params: { appKey?: string, connectionId?: string } = {};
|
||||
export const CREATE_FLOW = '/editor/create';
|
||||
export const CREATE_FLOW_WITH_APP = (appKey: string) =>
|
||||
`/editor/create?appKey=${appKey}`;
|
||||
export const CREATE_FLOW_WITH_APP_AND_CONNECTION = (
|
||||
appKey?: string,
|
||||
connectionId?: string
|
||||
) => {
|
||||
const params: { appKey?: string; connectionId?: string } = {};
|
||||
|
||||
if (appKey) {
|
||||
params.appKey = appKey;
|
||||
@@ -33,10 +47,10 @@ export const CREATE_FLOW_WITH_APP_AND_CONNECTION = (appKey?: string, connectionI
|
||||
params.connectionId = connectionId;
|
||||
}
|
||||
|
||||
const searchParams = (new URLSearchParams(params)).toString();
|
||||
const searchParams = new URLSearchParams(params).toString();
|
||||
|
||||
return `/editor/create?${searchParams}`;
|
||||
}
|
||||
};
|
||||
export const FLOW_EDITOR = (flowId: string): string => `/editor/${flowId}`;
|
||||
|
||||
export const FLOWS = '/flows';
|
||||
|
@@ -6,16 +6,19 @@ export type AuthenticationContextParams = {
|
||||
updateToken: (token: string) => void;
|
||||
};
|
||||
|
||||
export const AuthenticationContext = React.createContext<AuthenticationContextParams>({
|
||||
token: null,
|
||||
updateToken: () => void 0,
|
||||
});
|
||||
export const AuthenticationContext =
|
||||
React.createContext<AuthenticationContextParams>({
|
||||
token: null,
|
||||
updateToken: () => void 0,
|
||||
});
|
||||
|
||||
type AuthenticationProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const AuthenticationProvider = (props: AuthenticationProviderProps): React.ReactElement => {
|
||||
export const AuthenticationProvider = (
|
||||
props: AuthenticationProviderProps
|
||||
): React.ReactElement => {
|
||||
const { children } = props;
|
||||
const [token, setToken] = React.useState(() => getItem('token'));
|
||||
|
||||
@@ -30,10 +33,8 @@ export const AuthenticationProvider = (props: AuthenticationProviderProps): Reac
|
||||
}, [token]);
|
||||
|
||||
return (
|
||||
<AuthenticationContext.Provider
|
||||
value={value}
|
||||
>
|
||||
<AuthenticationContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthenticationContext.Provider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@@ -4,20 +4,20 @@ interface IEditorContext {
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export const EditorContext = React.createContext<IEditorContext>({ readOnly: false });
|
||||
export const EditorContext = React.createContext<IEditorContext>({
|
||||
readOnly: false,
|
||||
});
|
||||
|
||||
type EditorProviderProps = {
|
||||
children: React.ReactNode;
|
||||
value: IEditorContext;
|
||||
}
|
||||
};
|
||||
|
||||
export const EditorProvider = (props: EditorProviderProps): React.ReactElement => {
|
||||
export const EditorProvider = (
|
||||
props: EditorProviderProps
|
||||
): React.ReactElement => {
|
||||
const { children, value } = props;
|
||||
return (
|
||||
<EditorContext.Provider
|
||||
value={value}
|
||||
>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
<EditorContext.Provider value={value}>{children}</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@@ -6,15 +6,15 @@ export const StepExecutionsContext = React.createContext<IStep[]>([]);
|
||||
type StepExecutionsProviderProps = {
|
||||
children: React.ReactNode;
|
||||
value: IStep[];
|
||||
}
|
||||
};
|
||||
|
||||
export const StepExecutionsProvider = (props: StepExecutionsProviderProps): React.ReactElement => {
|
||||
export const StepExecutionsProvider = (
|
||||
props: StepExecutionsProviderProps
|
||||
): React.ReactElement => {
|
||||
const { children, value } = props;
|
||||
return (
|
||||
<StepExecutionsContext.Provider
|
||||
value={value}
|
||||
>
|
||||
<StepExecutionsContext.Provider value={value}>
|
||||
{children}
|
||||
</StepExecutionsContext.Provider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@@ -7,7 +7,7 @@ interface IRef {
|
||||
const cache = new InMemoryCache({
|
||||
typePolicies: {
|
||||
App: {
|
||||
keyFields: ['key']
|
||||
keyFields: ['key'],
|
||||
},
|
||||
Mutation: {
|
||||
mutationType: true,
|
||||
@@ -24,9 +24,11 @@ const cache = new InMemoryCache({
|
||||
id: appCacheId,
|
||||
fields: {
|
||||
connections: (existingConnections) => {
|
||||
const existingConnectionIndex = existingConnections.findIndex((connection: IRef) => {
|
||||
return connection.__ref === verifiedConnection.__ref;
|
||||
});
|
||||
const existingConnectionIndex = existingConnections.findIndex(
|
||||
(connection: IRef) => {
|
||||
return connection.__ref === verifiedConnection.__ref;
|
||||
}
|
||||
);
|
||||
const connectionExists = existingConnectionIndex !== -1;
|
||||
|
||||
// newly created and verified connection
|
||||
@@ -35,16 +37,16 @@ const cache = new InMemoryCache({
|
||||
}
|
||||
|
||||
return existingConnections;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return verifiedConnection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default cache;
|
||||
|
@@ -14,17 +14,19 @@ const client = new ApolloClient({
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function mutateAndGetClient(options: CreateClientOptions): typeof client {
|
||||
export function mutateAndGetClient(
|
||||
options: CreateClientOptions
|
||||
): typeof client {
|
||||
const { onError, token } = options;
|
||||
const link = createLink({ uri: appConfig.graphqlUrl, token, onError });
|
||||
|
||||
client.setLink(link);
|
||||
|
||||
return client;
|
||||
};
|
||||
}
|
||||
|
||||
export default client;
|
||||
|
@@ -11,52 +11,51 @@ type CreateLinkOptions = {
|
||||
onError?: (message: string) => void;
|
||||
};
|
||||
|
||||
const createHttpLink = (options: Pick<CreateLinkOptions, 'uri' | 'token'>): ApolloLink => {
|
||||
const createHttpLink = (
|
||||
options: Pick<CreateLinkOptions, 'uri' | 'token'>
|
||||
): ApolloLink => {
|
||||
const { uri, token } = options;
|
||||
const headers = {
|
||||
authorization: token,
|
||||
};
|
||||
return new HttpLink({ uri, headers });
|
||||
}
|
||||
};
|
||||
|
||||
const NOT_AUTHORISED = 'Not Authorised!';
|
||||
const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink => onError(({ graphQLErrors, networkError, operation }) => {
|
||||
const context = operation.getContext();
|
||||
const autoSnackbar = context.autoSnackbar ?? true;
|
||||
const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink =>
|
||||
onError(({ graphQLErrors, networkError, operation }) => {
|
||||
const context = operation.getContext();
|
||||
const autoSnackbar = context.autoSnackbar ?? true;
|
||||
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.forEach(({ message, locations, path }) => {
|
||||
if (autoSnackbar) {
|
||||
callback?.(message);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||
);
|
||||
|
||||
if (message === NOT_AUTHORISED) {
|
||||
setItem('token', '');
|
||||
window.location.href = URLS.LOGIN;
|
||||
}
|
||||
});
|
||||
|
||||
if (networkError) {
|
||||
if (autoSnackbar) {
|
||||
callback?.(message);
|
||||
callback?.(networkError.toString());
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
|
||||
);
|
||||
|
||||
if (message === NOT_AUTHORISED) {
|
||||
setItem('token', '');
|
||||
window.location.href = URLS.LOGIN;
|
||||
}
|
||||
});
|
||||
|
||||
if (networkError) {
|
||||
if (autoSnackbar) {
|
||||
callback?.(networkError.toString())
|
||||
console.log(`[Network error]: ${networkError}`);
|
||||
}
|
||||
console.log(`[Network error]: ${networkError}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const noop = () => { };
|
||||
const noop = () => {};
|
||||
|
||||
const createLink = (options: CreateLinkOptions): ApolloLink => {
|
||||
const {
|
||||
uri,
|
||||
onError = noop,
|
||||
token,
|
||||
} = options;
|
||||
const { uri, onError = noop, token } = options;
|
||||
|
||||
const httpOptions = { uri, token };
|
||||
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_CONNECTION = gql`
|
||||
mutation CreateConnection(
|
||||
$input: CreateConnectionInput
|
||||
) {
|
||||
mutation CreateConnection($input: CreateConnectionInput) {
|
||||
createConnection(input: $input) {
|
||||
id
|
||||
key
|
||||
|
@@ -6,8 +6,8 @@ import { DELETE_CONNECTION } from './delete-connection';
|
||||
import { CREATE_AUTH_DATA } from './create-auth-data';
|
||||
|
||||
type Mutations = {
|
||||
[key: string]: any,
|
||||
}
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const mutations: Mutations = {
|
||||
createConnection: CREATE_CONNECTION,
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import type { FieldPolicy, Reference } from '@apollo/client';
|
||||
|
||||
type KeyArgs = FieldPolicy<unknown>["keyArgs"];
|
||||
type KeyArgs = FieldPolicy<unknown>['keyArgs'];
|
||||
|
||||
export type TEdge<TNode> = {
|
||||
node: TNode;
|
||||
} | Reference;
|
||||
export type TEdge<TNode> =
|
||||
| {
|
||||
node: TNode;
|
||||
}
|
||||
| Reference;
|
||||
|
||||
export type TPageInfo = {
|
||||
currentPage: number;
|
||||
@@ -27,7 +29,6 @@ export type CustomFieldPolicy<TNode> = FieldPolicy<
|
||||
TIncoming<TNode> | null
|
||||
>;
|
||||
|
||||
|
||||
const makeEmptyData = <TNode>(): TExisting<TNode> => {
|
||||
return {
|
||||
edges: [],
|
||||
@@ -51,8 +52,7 @@ function offsetLimitPagination<TNode = Reference>(
|
||||
if (!incoming || incoming === null) return existing;
|
||||
|
||||
const existingEdges = existing?.edges || [];
|
||||
const incomingEdges = incoming.edges || []
|
||||
|
||||
const incomingEdges = incoming.edges || [];
|
||||
|
||||
if (args) {
|
||||
const newEdges = [...existingEdges, ...incomingEdges];
|
||||
|
@@ -1,8 +1,16 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_APPS = gql`
|
||||
query GetApps($name: String, $onlyWithTriggers: Boolean, $onlyWithActions: Boolean) {
|
||||
getApps(name: $name, onlyWithTriggers: $onlyWithTriggers, onlyWithActions: $onlyWithActions) {
|
||||
query GetApps(
|
||||
$name: String
|
||||
$onlyWithTriggers: Boolean
|
||||
$onlyWithActions: Boolean
|
||||
) {
|
||||
getApps(
|
||||
name: $name
|
||||
onlyWithTriggers: $onlyWithTriggers
|
||||
onlyWithActions: $onlyWithActions
|
||||
) {
|
||||
name
|
||||
key
|
||||
iconUrl
|
||||
|
@@ -13,4 +13,4 @@ export const GET_CONNECTED_APPS = gql`
|
||||
supportsConnections
|
||||
}
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
@@ -2,7 +2,11 @@ import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_EXECUTION_STEPS = gql`
|
||||
query GetExecutionSteps($executionId: String!, $limit: Int!, $offset: Int!) {
|
||||
getExecutionSteps(executionId: $executionId, limit: $limit, offset: $offset) {
|
||||
getExecutionSteps(
|
||||
executionId: $executionId
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
totalPages
|
||||
|
@@ -1,8 +1,20 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_FLOWS = gql`
|
||||
query GetFlows($limit: Int!, $offset: Int!, $appKey: String, $connectionId: String, $name: String) {
|
||||
getFlows(limit: $limit, offset: $offset, appKey: $appKey, connectionId: $connectionId, name: $name) {
|
||||
query GetFlows(
|
||||
$limit: Int!
|
||||
$offset: Int!
|
||||
$appKey: String
|
||||
$connectionId: String
|
||||
$name: String
|
||||
) {
|
||||
getFlows(
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
appKey: $appKey
|
||||
connectionId: $connectionId
|
||||
name: $name
|
||||
) {
|
||||
pageInfo {
|
||||
currentPage
|
||||
totalPages
|
||||
|
@@ -7,7 +7,10 @@ enum AuthenticationSteps {
|
||||
OpenWithPopup = 'openWithPopup',
|
||||
}
|
||||
|
||||
const processMutation = async (step: IAuthenticationStep, variables: IJSONObject) => {
|
||||
const processMutation = async (
|
||||
step: IAuthenticationStep,
|
||||
variables: IJSONObject
|
||||
) => {
|
||||
const mutation = MUTATIONS[step.name];
|
||||
const mutationResponse = await apolloClient.mutate({
|
||||
mutation,
|
||||
@@ -37,12 +40,20 @@ function getObjectOfEntries(iterator: any) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const processOpenWithPopup = (step: IAuthenticationStep, variables: IJSONObject) => {
|
||||
const processOpenWithPopup = (
|
||||
step: IAuthenticationStep,
|
||||
variables: IJSONObject
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const windowFeatures = 'toolbar=no, titlebar=no, menubar=no, width=500, height=700, top=100, left=100';
|
||||
const windowFeatures =
|
||||
'toolbar=no, titlebar=no, menubar=no, width=500, height=700, top=100, left=100';
|
||||
const url = variables.url;
|
||||
|
||||
const popup = window.open(url as string, '_blank', windowFeatures) as WindowProxy;
|
||||
const popup = window.open(
|
||||
url as string,
|
||||
'_blank',
|
||||
windowFeatures
|
||||
) as WindowProxy;
|
||||
popup?.focus();
|
||||
|
||||
const closeCheckIntervalId = setInterval(() => {
|
||||
@@ -50,7 +61,7 @@ const processOpenWithPopup = (step: IAuthenticationStep, variables: IJSONObject)
|
||||
clearInterval(closeCheckIntervalId);
|
||||
reject({ message: 'Error occured while verifying credentials!' });
|
||||
}
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
|
||||
const messageHandler = async (event: MessageEvent) => {
|
||||
if (event.data.source !== 'automatisch') {
|
||||
@@ -68,7 +79,10 @@ const processOpenWithPopup = (step: IAuthenticationStep, variables: IJSONObject)
|
||||
});
|
||||
};
|
||||
|
||||
export const processStep = async (step: IAuthenticationStep, variables: IJSONObject): Promise<any> => {
|
||||
export const processStep = async (
|
||||
step: IAuthenticationStep,
|
||||
variables: IJSONObject
|
||||
): Promise<any> => {
|
||||
if (step.type === AuthenticationSteps.Mutation) {
|
||||
return processMutation(step, variables);
|
||||
} else if (step.type === AuthenticationSteps.OpenWithPopup) {
|
||||
|
@@ -4,23 +4,32 @@ import type { IAuthenticationStepField, IJSONObject } from '@automatisch/types';
|
||||
const interpolate = /{([\s\S]+?)}/g;
|
||||
|
||||
type Variables = {
|
||||
[key: string]: any
|
||||
}
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type IVariable = Omit<IAuthenticationStepField, "properties"> & Partial<Pick<IAuthenticationStepField, "properties">>;
|
||||
type IVariable = Omit<IAuthenticationStepField, 'properties'> &
|
||||
Partial<Pick<IAuthenticationStepField, 'properties'>>;
|
||||
|
||||
const computeAuthStepVariables = (variableSchema: IVariable[], aggregatedData: IJSONObject): IJSONObject => {
|
||||
const computeAuthStepVariables = (
|
||||
variableSchema: IVariable[],
|
||||
aggregatedData: IJSONObject
|
||||
): IJSONObject => {
|
||||
const variables: Variables = {};
|
||||
|
||||
for (const variable of variableSchema) {
|
||||
if (variable.properties) {
|
||||
variables[variable.name] = computeAuthStepVariables(variable.properties, aggregatedData);
|
||||
variables[variable.name] = computeAuthStepVariables(
|
||||
variable.properties,
|
||||
aggregatedData
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variable.value) {
|
||||
const computedVariable = template(variable.value, { interpolate })(aggregatedData);
|
||||
const computedVariable = template(variable.value, { interpolate })(
|
||||
aggregatedData
|
||||
);
|
||||
|
||||
variables[variable.name] = computedVariable;
|
||||
}
|
||||
|
@@ -4,23 +4,32 @@ import type { IAuthenticationStepField, IJSONObject } from '@automatisch/types';
|
||||
const interpolate = /{([\s\S]+?)}/g;
|
||||
|
||||
type Variables = {
|
||||
[key: string]: any
|
||||
}
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
type IVariable = Omit<IAuthenticationStepField, "properties"> & Partial<Pick<IAuthenticationStepField, "properties">>;
|
||||
type IVariable = Omit<IAuthenticationStepField, 'properties'> &
|
||||
Partial<Pick<IAuthenticationStepField, 'properties'>>;
|
||||
|
||||
const computeAuthStepVariables = (variableSchema: IVariable[], aggregatedData: IJSONObject): IJSONObject => {
|
||||
const computeAuthStepVariables = (
|
||||
variableSchema: IVariable[],
|
||||
aggregatedData: IJSONObject
|
||||
): IJSONObject => {
|
||||
const variables: Variables = {};
|
||||
|
||||
for (const variable of variableSchema) {
|
||||
if (variable.properties) {
|
||||
variables[variable.name] = computeAuthStepVariables(variable.properties, aggregatedData);
|
||||
variables[variable.name] = computeAuthStepVariables(
|
||||
variable.properties,
|
||||
aggregatedData
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variable.value) {
|
||||
const computedVariable = template(variable.value, { interpolate })(aggregatedData);
|
||||
const computedVariable = template(variable.value, { interpolate })(
|
||||
aggregatedData
|
||||
);
|
||||
|
||||
variables[variable.name] = computedVariable;
|
||||
}
|
||||
|
@@ -2,4 +2,4 @@ import copy from 'clipboard-copy';
|
||||
|
||||
export default function copyInputValue(element: HTMLInputElement): void {
|
||||
copy(element.value);
|
||||
};
|
||||
}
|
||||
|
@@ -7,4 +7,4 @@ export const setItem = (key: string, value: string) => {
|
||||
|
||||
export const getItem = (key: string) => {
|
||||
return localStorage.getItem(makeKey(key));
|
||||
}
|
||||
};
|
||||
|
@@ -4,8 +4,8 @@ import type { AuthenticationContextParams } from 'contexts/Authentication';
|
||||
|
||||
type UseAuthenticationReturn = {
|
||||
isAuthenticated: boolean;
|
||||
token: AuthenticationContextParams["token"];
|
||||
updateToken: AuthenticationContextParams["updateToken"];
|
||||
token: AuthenticationContextParams['token'];
|
||||
updateToken: AuthenticationContextParams['updateToken'];
|
||||
};
|
||||
|
||||
export default function useAuthentication(): UseAuthenticationReturn {
|
||||
|
@@ -4,37 +4,41 @@ import { useFormContext } from 'react-hook-form';
|
||||
import set from 'lodash/set';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import type { IField, IFieldDropdownSource, IJSONObject } from '@automatisch/types';
|
||||
import type {
|
||||
IField,
|
||||
IFieldDropdownSource,
|
||||
IJSONObject,
|
||||
} from '@automatisch/types';
|
||||
|
||||
import { GET_DATA } from 'graphql/queries/get-data';
|
||||
|
||||
const variableRegExp = /({.*?})/g;
|
||||
|
||||
function computeArguments(args: IFieldDropdownSource["arguments"], getValues: UseFormReturn["getValues"]): IJSONObject {
|
||||
function computeArguments(
|
||||
args: IFieldDropdownSource['arguments'],
|
||||
getValues: UseFormReturn['getValues']
|
||||
): IJSONObject {
|
||||
const initialValue = {};
|
||||
return args.reduce(
|
||||
(result, { name, value }) => {
|
||||
const isVariable = variableRegExp.test(value);
|
||||
return args.reduce((result, { name, value }) => {
|
||||
const isVariable = variableRegExp.test(value);
|
||||
|
||||
if (isVariable) {
|
||||
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||
const computedValue = getValues(sanitizedFieldPath);
|
||||
if (isVariable) {
|
||||
const sanitizedFieldPath = value.replace(/{|}/g, '');
|
||||
const computedValue = getValues(sanitizedFieldPath);
|
||||
|
||||
if (computedValue === undefined) throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||
if (computedValue === undefined)
|
||||
throw new Error(`The ${sanitizedFieldPath} field is required.`);
|
||||
|
||||
set(result, name, computedValue);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
set(result, name, value);
|
||||
set(result, name, computedValue);
|
||||
|
||||
return result;
|
||||
},
|
||||
initialValue
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
set(result, name, value);
|
||||
|
||||
return result;
|
||||
}, initialValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the dynamic data for the given step.
|
||||
@@ -81,13 +85,18 @@ function useDynamicData(stepId: string | undefined, schema: IField) {
|
||||
}, [schema, formValues, getValues]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (schema.type === 'dropdown' && stepId && schema.source && computedVariables) {
|
||||
if (
|
||||
schema.type === 'dropdown' &&
|
||||
stepId &&
|
||||
schema.source &&
|
||||
computedVariables
|
||||
) {
|
||||
getData({
|
||||
variables: {
|
||||
stepId,
|
||||
...computedVariables,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [getData, stepId, schema, computedVariables]);
|
||||
|
||||
|
@@ -1,10 +1,13 @@
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
type Values = {
|
||||
[key: string]: any,
|
||||
}
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export default function useFormatMessage(): (id: string, values?: Values) => string {
|
||||
export default function useFormatMessage(): (
|
||||
id: string,
|
||||
values?: Values
|
||||
) => string {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (id: string, values: Values = {}) => formatMessage({ id }, values);
|
||||
|
@@ -1,14 +1,13 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { compare } from 'compare-versions';
|
||||
|
||||
|
||||
import { HEALTHCHECK } from 'graphql/queries/healthcheck';
|
||||
import useNotifications from 'hooks/useNotifications';
|
||||
|
||||
type TVersionInfo = {
|
||||
version: string;
|
||||
newVersionCount: number;
|
||||
}
|
||||
};
|
||||
|
||||
export default function useVersion(): TVersionInfo {
|
||||
const notifications = useNotifications();
|
||||
|
@@ -15,16 +15,14 @@ ReactDOM.render(
|
||||
<ApolloProvider>
|
||||
<IntlProvider>
|
||||
<ThemeProvider>
|
||||
<Router>
|
||||
{routes}
|
||||
</Router>
|
||||
<Router>{routes}</Router>
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
</ApolloProvider>
|
||||
</AuthenticationProvider>
|
||||
</SnackbarProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
|
@@ -86,4 +86,4 @@
|
||||
"profileSettings.updatePassword": "Update password",
|
||||
"notifications.title": "Notifications",
|
||||
"notification.releasedAt": "Released {relativeDate}"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { Link, Route, Navigate, Routes, useParams, useSearchParams, useMatch, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Link,
|
||||
Route,
|
||||
Navigate,
|
||||
Routes,
|
||||
useParams,
|
||||
useSearchParams,
|
||||
useMatch,
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
import type { LinkProps } from 'react-router-dom';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
@@ -38,13 +47,18 @@ const ReconnectConnection = (props: any): React.ReactElement => {
|
||||
connectionId={connectionId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function Application(): React.ReactElement | null {
|
||||
const theme = useTheme();
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), { noSsr: true });
|
||||
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'), {
|
||||
noSsr: true,
|
||||
});
|
||||
const formatMessage = useFormatMessage();
|
||||
const connectionsPathMatch = useMatch({ path: URLS.APP_CONNECTIONS_PATTERN, end: false });
|
||||
const connectionsPathMatch = useMatch({
|
||||
path: URLS.APP_CONNECTIONS_PATTERN,
|
||||
end: false,
|
||||
});
|
||||
const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false });
|
||||
const [searchParams] = useSearchParams();
|
||||
const { appKey } = useParams() as ApplicationParams;
|
||||
@@ -57,24 +71,37 @@ export default function Application(): React.ReactElement | null {
|
||||
|
||||
const NewConnectionLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
|
||||
linkProps,
|
||||
ref,
|
||||
) {
|
||||
return <Link ref={ref} to={URLS.APP_ADD_CONNECTION(appKey)} {...linkProps} />;
|
||||
}),
|
||||
[appKey],
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
||||
function InlineLink(linkProps, ref) {
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
to={URLS.APP_ADD_CONNECTION(appKey)}
|
||||
{...linkProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
),
|
||||
[appKey]
|
||||
);
|
||||
|
||||
const NewFlowLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
|
||||
linkProps,
|
||||
ref,
|
||||
) {
|
||||
return <Link ref={ref} to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(appKey, connectionId)} {...linkProps} />;
|
||||
}),
|
||||
[appKey, connectionId],
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
||||
function InlineLink(linkProps, ref) {
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
|
||||
appKey,
|
||||
connectionId
|
||||
)}
|
||||
{...linkProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
),
|
||||
[appKey, connectionId]
|
||||
);
|
||||
|
||||
if (loading) return null;
|
||||
@@ -141,7 +168,10 @@ export default function Application(): React.ReactElement | null {
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs
|
||||
variant={matchSmallScreens ? 'fullWidth' : undefined}
|
||||
value={connectionsPathMatch?.pattern?.path || flowsPathMatch?.pattern?.path}
|
||||
value={
|
||||
connectionsPathMatch?.pattern?.path ||
|
||||
flowsPathMatch?.pattern?.path
|
||||
}
|
||||
>
|
||||
<Tab
|
||||
label={formatMessage('app.connections')}
|
||||
@@ -161,15 +191,28 @@ export default function Application(): React.ReactElement | null {
|
||||
</Box>
|
||||
|
||||
<Routes>
|
||||
<Route path={`${URLS.FLOWS}/*`} element={<AppFlows appKey={appKey} />} />
|
||||
<Route
|
||||
path={`${URLS.FLOWS}/*`}
|
||||
element={<AppFlows appKey={appKey} />}
|
||||
/>
|
||||
|
||||
<Route path={`${URLS.CONNECTIONS}/*`} element={<AppConnections appKey={appKey} />} />
|
||||
<Route
|
||||
path={`${URLS.CONNECTIONS}/*`}
|
||||
element={<AppConnections appKey={appKey} />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/"
|
||||
element={(
|
||||
<Navigate to={app.supportsConnections ? URLS.APP_CONNECTIONS(appKey) : URLS.APP_FLOWS(appKey)} replace />
|
||||
)}
|
||||
element={
|
||||
<Navigate
|
||||
to={
|
||||
app.supportsConnections
|
||||
? URLS.APP_CONNECTIONS(appKey)
|
||||
: URLS.APP_FLOWS(appKey)
|
||||
}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Grid>
|
||||
@@ -181,18 +224,20 @@ export default function Application(): React.ReactElement | null {
|
||||
<Route
|
||||
path="/connections/add"
|
||||
element={
|
||||
<AddAppConnection
|
||||
onClose={goToApplicationPage}
|
||||
application={app}
|
||||
/>
|
||||
<AddAppConnection onClose={goToApplicationPage} application={app} />
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/connections/:connectionId/reconnect"
|
||||
element={<ReconnectConnection application={app} onClose={goToApplicationPage} />}
|
||||
element={
|
||||
<ReconnectConnection
|
||||
application={app}
|
||||
onClose={goToApplicationPage}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ import AddNewAppConnection from 'components/AddNewAppConnection';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import AppRow from 'components/AppRow';
|
||||
import SearchInput from 'components/SearchInput';
|
||||
import useFormatMessage from 'hooks/useFormatMessage'
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { GET_CONNECTED_APPS } from 'graphql/queries/get-connected-apps';
|
||||
import * as URLS from 'config/urls';
|
||||
|
||||
@@ -24,7 +24,9 @@ export default function Applications(): React.ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const formatMessage = useFormatMessage();
|
||||
const [appName, setAppName] = React.useState(null);
|
||||
const { data, loading } = useQuery(GET_CONNECTED_APPS, { variables: {name: appName } });
|
||||
const { data, loading } = useQuery(GET_CONNECTED_APPS, {
|
||||
variables: { name: appName },
|
||||
});
|
||||
|
||||
const apps: IApp[] = data?.getConnectedApps;
|
||||
const hasApps = apps?.length;
|
||||
@@ -39,13 +41,12 @@ export default function Applications(): React.ReactElement {
|
||||
|
||||
const NewAppConnectionLink = React.useMemo(
|
||||
() =>
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(function InlineLink(
|
||||
linkProps,
|
||||
ref,
|
||||
) {
|
||||
return <Link ref={ref} to={URLS.NEW_APP_CONNECTION} {...linkProps} />;
|
||||
}),
|
||||
[],
|
||||
React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'to'>>(
|
||||
function InlineLink(linkProps, ref) {
|
||||
return <Link ref={ref} to={URLS.NEW_APP_CONNECTION} {...linkProps} />;
|
||||
}
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -60,7 +61,14 @@ export default function Applications(): React.ReactElement {
|
||||
<SearchInput onChange={onSearchChange} />
|
||||
</Grid>
|
||||
|
||||
<Grid container item xs="auto" sm="auto" alignItems="center" order={{ xs: 1, sm: 2 }}>
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
xs="auto"
|
||||
sm="auto"
|
||||
alignItems="center"
|
||||
order={{ xs: 1, sm: 2 }}
|
||||
>
|
||||
<ConditionalIconButton
|
||||
type="submit"
|
||||
variant="contained"
|
||||
@@ -78,22 +86,30 @@ export default function Applications(): React.ReactElement {
|
||||
|
||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||
|
||||
{loading && <CircularProgress data-test="apps-loader" sx={{ display: 'block', margin: '20px auto' }} />}
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
data-test="apps-loader"
|
||||
sx={{ display: 'block', margin: '20px auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && !hasApps && (<NoResultFound
|
||||
text={formatMessage('apps.noConnections')}
|
||||
to={URLS.NEW_APP_CONNECTION}
|
||||
/>)}
|
||||
{!loading && !hasApps && (
|
||||
<NoResultFound
|
||||
text={formatMessage('apps.noConnections')}
|
||||
to={URLS.NEW_APP_CONNECTION}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{!loading && apps?.map((app: IApp) => (
|
||||
<AppRow key={app.name} application={app} />
|
||||
))}
|
||||
{!loading &&
|
||||
apps?.map((app: IApp) => <AppRow key={app.name} application={app} />)}
|
||||
|
||||
<Routes>
|
||||
<Route path="/new" element={<AddNewAppConnection onClose={goToApps} />} />
|
||||
<Route
|
||||
path="/new"
|
||||
element={<AddNewAppConnection onClose={goToApps} />}
|
||||
/>
|
||||
</Routes>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
export default function Dashboard() {
|
||||
return (<>Dashboard</>);
|
||||
};
|
||||
return <>Dashboard</>;
|
||||
}
|
||||
|
@@ -33,8 +33,8 @@ export default function CreateFlow(): React.ReactElement {
|
||||
|
||||
const response = await createFlow({
|
||||
variables: {
|
||||
input: variables
|
||||
}
|
||||
input: variables,
|
||||
},
|
||||
});
|
||||
const flowId = response.data?.createFlow?.id;
|
||||
|
||||
@@ -45,12 +45,21 @@ export default function CreateFlow(): React.ReactElement {
|
||||
}, [createFlow, navigate, appKey, connectionId]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flex: 1, height: '100vh', justifyContent: 'center', alignItems: 'center', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
height: '100vh',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={16} thickness={7.5} />
|
||||
|
||||
<Typography variant="body2">
|
||||
{formatMessage('createFlow.creating')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -2,7 +2,5 @@ import * as React from 'react';
|
||||
import EditorLayout from 'components/EditorLayout';
|
||||
|
||||
export default function FlowEditor(): React.ReactElement {
|
||||
return (
|
||||
<EditorLayout />
|
||||
)
|
||||
}
|
||||
return <EditorLayout />;
|
||||
}
|
||||
|
@@ -10,5 +10,5 @@ export default function EditorRoutes(): React.ReactElement {
|
||||
|
||||
<Route path="/:flowId" element={<EditorPage />} />
|
||||
</Routes>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@@ -28,31 +28,43 @@ const getLimitAndOffset = (page: number) => ({
|
||||
export default function Execution(): React.ReactElement {
|
||||
const { executionId } = useParams() as ExecutionParams;
|
||||
const formatMessage = useFormatMessage();
|
||||
const { data: execution } = useQuery(GET_EXECUTION, { variables: { executionId } });
|
||||
const { data, loading } = useQuery(GET_EXECUTION_STEPS, { variables: { executionId, ...getLimitAndOffset(1) } });
|
||||
const { data: execution } = useQuery(GET_EXECUTION, {
|
||||
variables: { executionId },
|
||||
});
|
||||
const { data, loading } = useQuery(GET_EXECUTION_STEPS, {
|
||||
variables: { executionId, ...getLimitAndOffset(1) },
|
||||
});
|
||||
|
||||
const { edges } = data?.getExecutionSteps || {};
|
||||
const executionSteps: IExecutionStep[] = edges?.map((edge: { node: IExecutionStep }) => edge.node);
|
||||
const executionSteps: IExecutionStep[] = edges?.map(
|
||||
(edge: { node: IExecutionStep }) => edge.node
|
||||
);
|
||||
|
||||
return (
|
||||
<Container sx={{ py: 3 }}>
|
||||
<ExecutionHeader
|
||||
execution={execution?.getExecution}
|
||||
/>
|
||||
<ExecutionHeader execution={execution?.getExecution} />
|
||||
|
||||
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
||||
{!loading && !executionSteps?.length && (
|
||||
<Alert severity="warning" sx={{ flex: 1 }}>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>{formatMessage('execution.noDataTitle')}</AlertTitle>
|
||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
||||
{formatMessage('execution.noDataTitle')}
|
||||
</AlertTitle>
|
||||
|
||||
<Box sx={{ fontWeight: 400 }}>{formatMessage('execution.noDataMessage')}</Box>
|
||||
<Box sx={{ fontWeight: 400 }}>
|
||||
{formatMessage('execution.noDataMessage')}
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{executionSteps?.map((executionStep) => (
|
||||
<ExecutionStep key={executionStep.id} executionStep={executionStep} step={executionStep.step} />
|
||||
<ExecutionStep
|
||||
key={executionStep.id}
|
||||
executionStep={executionStep}
|
||||
step={executionStep.step}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import Box from '@mui/material/Box';
|
||||
import Grid from '@mui/material/Grid';
|
||||
@@ -13,7 +13,7 @@ import NoResultFound from 'components/NoResultFound';
|
||||
import ExecutionRow from 'components/ExecutionRow';
|
||||
import Container from 'components/Container';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import useFormatMessage from 'hooks/useFormatMessage'
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import { GET_EXECUTIONS } from 'graphql/queries/get-executions';
|
||||
|
||||
const EXECUTION_PER_PAGE = 10;
|
||||
@@ -38,44 +38,65 @@ export default function Executions(): React.ReactElement {
|
||||
|
||||
React.useEffect(() => {
|
||||
refetch(getLimitAndOffset(page));
|
||||
}, [refetch, page])
|
||||
}, [refetch, page]);
|
||||
|
||||
const executions: IExecution[] = edges?.map(({ node }: { node: IExecution }) => node);
|
||||
const executions: IExecution[] = edges?.map(
|
||||
({ node }: { node: IExecution }) => node
|
||||
);
|
||||
const hasExecutions = executions?.length;
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Container>
|
||||
<Grid container sx={{ mb: [0, 3] }} columnSpacing={1.5} rowSpacing={3}>
|
||||
<Grid container item xs sm alignItems="center" order={{ xs: 0, height: 80 }}>
|
||||
<Grid
|
||||
container
|
||||
item
|
||||
xs
|
||||
sm
|
||||
alignItems="center"
|
||||
order={{ xs: 0, height: 80 }}
|
||||
>
|
||||
<PageTitle>{formatMessage('executions.title')}</PageTitle>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||
|
||||
{loading && <CircularProgress data-test="executions-loader" sx={{ display: 'block', margin: '20px auto' }} />}
|
||||
{loading && (
|
||||
<CircularProgress
|
||||
data-test="executions-loader"
|
||||
sx={{ display: 'block', margin: '20px auto' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading && !hasExecutions && (<NoResultFound
|
||||
text={formatMessage('executions.noExecutions')}
|
||||
/>)}
|
||||
{!loading && !hasExecutions && (
|
||||
<NoResultFound text={formatMessage('executions.noExecutions')} />
|
||||
)}
|
||||
|
||||
{!loading && executions?.map((execution) => (<ExecutionRow key={execution.id} execution={execution} />))}
|
||||
{!loading &&
|
||||
executions?.map((execution) => (
|
||||
<ExecutionRow key={execution.id} execution={execution} />
|
||||
))}
|
||||
|
||||
{pageInfo && pageInfo.totalPages > 1 && <Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
onChange={(event, page) => setSearchParams({ page: page.toString() })}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>}
|
||||
{pageInfo && pageInfo.totalPages > 1 && (
|
||||
<Pagination
|
||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||
page={pageInfo?.currentPage}
|
||||
count={pageInfo?.totalPages}
|
||||
onChange={(event, page) =>
|
||||
setSearchParams({ page: page.toString() })
|
||||
}
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
component={Link}
|
||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
||||
{...item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user