Merge pull request #373 from automatisch/issue-367

Enhance flow row with used apps, date and context menu
This commit is contained in:
Ömer Faruk Aydın
2022-08-07 01:11:22 +03:00
committed by GitHub
16 changed files with 267 additions and 28 deletions

View File

@@ -203,6 +203,8 @@ type Flow {
name: String
active: Boolean
steps: [Step]
createdAt: String
updatedAt: String
}
type Execution {

View File

@@ -31,9 +31,9 @@ class Base extends Model {
}
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise<void> {
await super.$beforeUpdate(opt, queryContext);
this.updatedAt = new Date().toISOString();
await super.$beforeUpdate(opt, queryContext);
}
}

View File

@@ -44,7 +44,9 @@ class Flow extends Base {
},
});
async $beforeUpdate(opt: ModelOptions): Promise<void> {
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise<void> {
await super.$beforeUpdate(opt, queryContext);
if (!this.active) return;
const oldFlow = opt.old as Flow;

View File

@@ -73,6 +73,8 @@ class Step extends Base {
});
get iconUrl() {
if (!this.appKey) return null;
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
}

View File

@@ -26,6 +26,7 @@ export interface IExecutionStep {
dataOut: IJSONObject;
status: string;
createdAt: string;
updatedAt: string;
}
export interface IExecution {
@@ -34,6 +35,7 @@ export interface IExecution {
flow: IFlow;
testRun: boolean;
executionSteps: IExecutionStep[];
updatedAt: string;
createdAt: string;
}
@@ -43,6 +45,7 @@ export interface IStep {
flowId: string;
key: string;
appKey: string;
iconUrl: string;
type: 'action' | 'trigger';
connectionId: string;
status: string;
@@ -61,6 +64,8 @@ export interface IFlow {
userId: string;
active: boolean;
steps: IStep[];
createdAt: string;
updatedAt: string;
}
export interface IUser {

View File

@@ -6,6 +6,7 @@ type AppIconProps = {
name?: string;
url?: string;
color?: string;
variant?: AvatarProps['variant'];
};
const inlineImgStyle: React.CSSProperties = {
@@ -13,16 +14,26 @@ const inlineImgStyle: React.CSSProperties = {
};
export default function AppIcon(props: AppIconProps & AvatarProps): React.ReactElement {
const { name, url, color, sx = {}, ...restProps } = props;
const {
name,
url,
color,
sx = {},
variant = "square",
...restProps
} = props;
const initialLetter = name?.[0];
return (
<Avatar
component="span"
variant="square"
variant={variant}
sx={{ bgcolor: color, display: 'flex', width: 50, height: 50, ...sx }}
imgProps={{ style: inlineImgStyle }}
src={url}
alt={name}
children={initialLetter}
{...restProps}
/>
);

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
import type { IStep } from '@automatisch/types';
import AppIcon from 'components/AppIcon';
import IntermediateStepCount from 'components/IntermediateStepCount';
type FlowAppIconsProps = {
steps: Partial<IStep>[];
}
export default function FlowAppIcons(props: FlowAppIconsProps) {
const { steps } = props;
const stepsCount = steps.length;
const firstStep = steps[0];
const lastStep = steps[stepsCount - 1];
const intermeaditeStepCount = stepsCount - 2;
return (
<>
<AppIcon
name=" "
variant="rounded"
url={firstStep.iconUrl}
imgProps={{ width: 30, height: 30}}
/>
{intermeaditeStepCount > 0 && <IntermediateStepCount count={intermeaditeStepCount} />}
<AppIcon
name=" "
variant="rounded"
url={lastStep.iconUrl}
/>
</>
)
};

View File

@@ -0,0 +1,58 @@
import * as React from 'react';
import { useMutation } from '@apollo/client';
import { Link } from 'react-router-dom';
import Menu from '@mui/material/Menu';
import type { PopoverProps } from '@mui/material/Popover';
import MenuItem from '@mui/material/MenuItem';
import { DELETE_FLOW } from 'graphql/mutations/delete-flow';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
type ContextMenuProps = {
flowId: string;
onClose: () => void;
anchorEl: PopoverProps['anchorEl'];
};
export default function ContextMenu(props: ContextMenuProps): React.ReactElement {
const { flowId, onClose, anchorEl } = props;
const [deleteFlow] = useMutation(DELETE_FLOW);
const formatMessage = useFormatMessage();
const onFlowDelete = React.useCallback(async () => {
await deleteFlow({
variables: { input: { id: flowId } },
update: (cache) => {
const flowCacheId = cache.identify({
__typename: 'Flow',
id: flowId,
});
cache.evict({
id: flowCacheId,
});
}
});
}, [flowId, deleteFlow]);
return (
<Menu
open={true}
onClose={onClose}
hideBackdrop={false}
anchorEl={anchorEl}
>
<MenuItem
component={Link}
to={URLS.FLOW(flowId)}
>
{formatMessage('flow.view')}
</MenuItem>
<MenuItem onClick={onFlowDelete}>
{formatMessage('flow.delete')}
</MenuItem>
</Menu>
);
};

View File

@@ -1,38 +1,90 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import Card from '@mui/material/Card';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import CardActionArea from '@mui/material/CardActionArea';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { DateTime } from 'luxon';
import type { IFlow } from '@automatisch/types';
import FlowAppIcons from 'components/FlowAppIcons';
import FlowContextMenu from 'components/FlowContextMenu';
import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls';
import { CardContent, Typography } from './style';
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 { flow } = props;
return (
<Link to={URLS.FLOW(flow.id)}>
<Card sx={{ mb: 1 }}>
<CardActionArea>
<CardContent>
<Box display="flex" flex={1}>
<Typography variant="h6" noWrap>
{flow.name}
</Typography>
</Box>
const handleClose = () => {
setAnchorEl(null);
};
const onContextMenuClick = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
setAnchorEl(contextButtonRef.current);
}
<Box>
<ArrowForwardIosIcon sx={{ color: (theme) => theme.palette.primary.main }} />
</Box>
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10));
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10));
const isUpdated = updatedAt > createdAt;
const relativeCreatedAt = createdAt.toRelative();
const relativeUpdatedAt = updatedAt.toRelative();
return (
<>
<Card sx={{ mb: 1 }}>
<CardActionArea component={Link} to={URLS.FLOW(flow.id)}>
<CardContent>
<Apps direction="row" gap={1} sx={{gridArea:"apps"}}>
<FlowAppIcons steps={flow.steps} />
</Apps>
<Title
justifyContent="center"
alignItems="flex-start"
spacing={1}
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 })}
</Typography>
</Title>
<ContextMenu>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="open context menu"
ref={contextButtonRef}
onClick={onContextMenuClick}
>
<MoreHorizIcon />
</IconButton>
</ContextMenu>
</CardContent>
</CardActionArea>
</Card>
</Link>
{anchorEl && <FlowContextMenu
flowId={flow.id}
onClose={handleClose}
anchorEl={anchorEl}
/>}
</>
);
}

View File

@@ -1,20 +1,42 @@
import { styled } from '@mui/material/styles';
import MuiStack from '@mui/material/Stack';
import MuiBox from '@mui/material/Box';
import MuiCardContent from '@mui/material/CardContent';
import MuiTypography from '@mui/material/Typography';
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
display: 'grid',
gridTemplateRows: 'auto',
gridTemplateColumns: '1fr auto',
gridColumnGap: theme.spacing(2),
gridTemplateColumns: 'calc(50px * 3 + 8px * 2) minmax(0, auto) min-content',
gridGap: theme.spacing(2),
gridTemplateAreas: `
"apps title menu"
`,
alignItems: 'center',
[theme.breakpoints.down('sm')]: {
gridTemplateAreas: `
"apps menu"
"title menu"
`,
gridTemplateColumns: 'minmax(0, auto) min-content',
gridTemplateRows: 'auto auto',
}
}));
export const Apps = styled(MuiStack)(() => ({
gridArea: 'apps',
}));
export const Title = styled(MuiStack)(() => ({
gridArea: 'title',
}));
export const ContextMenu = styled(MuiBox)(() => ({
gridArea: 'menu',
}));
export const Typography = styled(MuiTypography)(() => ({
display: 'inline-block',
width: '100%',
maxWidth: '70%',
maxWidth: '85%',
}));
export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({

View File

@@ -106,7 +106,7 @@ export default function FlowStep(
);
const isTrigger = step.type === 'trigger';
const formatMessage = useFormatMessage();
const [currentSubstep, setCurrentSubstep] = React.useState<number | null>(2);
const [currentSubstep, setCurrentSubstep] = React.useState<number | null>(0);
const { data } = useQuery(GET_APPS, {
variables: { onlyWithTriggers: isTrigger },
});

View File

@@ -0,0 +1,20 @@
import * as React from 'react';
import Typography from '@mui/material/Typography';
import { Container } from './style';
type IntermediateStepCountProps = {
count: number;
}
export default function IntermediateStepCount(props: IntermediateStepCountProps) {
const { count } = props;
return (
<Container>
<Typography variant="subtitle1" sx={{ }}>
+{count}
</Typography>
</Container>
);
}

View File

@@ -0,0 +1,12 @@
import { styled } from '@mui/material/styles';
export const Container = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
minWidth: 50,
height: 50,
border: `1px solid ${theme.palette.text.disabled}`,
borderRadius: theme.shape.borderRadius,
}));

View File

@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_FLOW = gql`
mutation DeleteFlow($input: DeleteFlowInput) {
deleteFlow(input: $input)
}
`;

View File

@@ -11,6 +11,11 @@ export const GET_FLOWS = gql`
node {
id
name
createdAt
updatedAt
steps {
iconUrl
}
}
}
}

View File

@@ -39,6 +39,10 @@
"createFlow.creating": "Creating a flow...",
"flow.active": "ON",
"flow.inactive": "OFF",
"flow.createdAt": "created {datetime}",
"flow.updatedAt": "updated {datetime}",
"flow.view": "View",
"flow.delete": "Delete",
"flowStep.triggerType": "Trigger",
"flowStep.actionType": "Action",
"flows.create": "Create flow",