feat: add relative time and context menu in flows
This commit is contained in:
@@ -203,6 +203,8 @@ type Flow {
|
||||
name: String
|
||||
active: Boolean
|
||||
steps: [Step]
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
}
|
||||
|
||||
type Execution {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
4
packages/types/index.d.ts
vendored
4
packages/types/index.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -61,6 +63,8 @@ export interface IFlow {
|
||||
userId: string;
|
||||
active: boolean;
|
||||
steps: IStep[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
|
58
packages/web/src/components/FlowContextMenu/index.tsx
Normal file
58
packages/web/src/components/FlowContextMenu/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
@@ -2,10 +2,15 @@ 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 Stack from '@mui/material/Stack';
|
||||
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 FlowContextMenu from 'components/FlowContextMenu';
|
||||
import useFormatMessage from 'hooks/useFormatMessage';
|
||||
import * as URLS from 'config/urls';
|
||||
import { CardContent, Typography } from './style';
|
||||
|
||||
@@ -14,25 +19,68 @@ type FlowRowProps = {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const onContextMenuClick = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
setAnchorEl(contextButtonRef.current);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Link to={URLS.FLOW(flow.id)}>
|
||||
<>
|
||||
<Card sx={{ mb: 1 }}>
|
||||
<CardActionArea>
|
||||
<CardActionArea component={Link} to={URLS.FLOW(flow.id)}>
|
||||
<CardContent>
|
||||
<Box display="flex" flex={1}>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
alignItems="flex-start"
|
||||
spacing={1}
|
||||
>
|
||||
<Typography variant="h6" noWrap>
|
||||
{flow.name}
|
||||
{flow?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption">
|
||||
{isUpdated && formatMessage('flow.updatedAt', { datetime: relativeUpdatedAt })}
|
||||
{!isUpdated && formatMessage('flow.createdAt', { datetime: relativeCreatedAt })}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<ArrowForwardIosIcon sx={{ color: (theme) => theme.palette.primary.main }} />
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="open context menu"
|
||||
ref={contextButtonRef}
|
||||
onClick={onContextMenuClick}
|
||||
>
|
||||
<MoreHorizIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{anchorEl && <FlowContextMenu
|
||||
flowId={flow.id}
|
||||
onClose={handleClose}
|
||||
anchorEl={anchorEl}
|
||||
/>}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -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 },
|
||||
});
|
||||
|
7
packages/web/src/graphql/mutations/delete-flow.ts
Normal file
7
packages/web/src/graphql/mutations/delete-flow.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_FLOW = gql`
|
||||
mutation DeleteFlow($input: DeleteFlowInput) {
|
||||
deleteFlow(input: $input)
|
||||
}
|
||||
`;
|
@@ -11,6 +11,8 @@ export const GET_FLOWS = gql`
|
||||
node {
|
||||
id
|
||||
name
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
Reference in New Issue
Block a user