From 15aaada3fe4ac73d6f09daf397abe7dfdf0ac7fb Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Sun, 31 Jul 2022 13:36:28 +0200 Subject: [PATCH] feat: add relative time and context menu in flows --- packages/backend/src/graphql/schema.graphql | 2 + packages/backend/src/models/base.ts | 4 +- packages/backend/src/models/flow.ts | 4 +- packages/types/index.d.ts | 4 ++ .../src/components/FlowContextMenu/index.tsx | 58 +++++++++++++++++ packages/web/src/components/FlowRow/index.tsx | 64 ++++++++++++++++--- .../web/src/components/FlowStep/index.tsx | 2 +- .../web/src/graphql/mutations/delete-flow.ts | 7 ++ packages/web/src/graphql/queries/get-flows.ts | 2 + packages/web/src/locales/en.json | 4 ++ 10 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 packages/web/src/components/FlowContextMenu/index.tsx create mode 100644 packages/web/src/graphql/mutations/delete-flow.ts diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 37b70553..242893ec 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -203,6 +203,8 @@ type Flow { name: String active: Boolean steps: [Step] + createdAt: String + updatedAt: String } type Execution { diff --git a/packages/backend/src/models/base.ts b/packages/backend/src/models/base.ts index 1d982bd6..95c40746 100644 --- a/packages/backend/src/models/base.ts +++ b/packages/backend/src/models/base.ts @@ -31,9 +31,9 @@ class Base extends Model { } async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise { - await super.$beforeUpdate(opt, queryContext); - this.updatedAt = new Date().toISOString(); + + await super.$beforeUpdate(opt, queryContext); } } diff --git a/packages/backend/src/models/flow.ts b/packages/backend/src/models/flow.ts index 727f2506..01733ded 100644 --- a/packages/backend/src/models/flow.ts +++ b/packages/backend/src/models/flow.ts @@ -44,7 +44,9 @@ class Flow extends Base { }, }); - async $beforeUpdate(opt: ModelOptions): Promise { + async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext): Promise { + await super.$beforeUpdate(opt, queryContext); + if (!this.active) return; const oldFlow = opt.old as Flow; diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 67b12115..e61a259f 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -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 { diff --git a/packages/web/src/components/FlowContextMenu/index.tsx b/packages/web/src/components/FlowContextMenu/index.tsx new file mode 100644 index 00000000..95bea760 --- /dev/null +++ b/packages/web/src/components/FlowContextMenu/index.tsx @@ -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 ( + + + {formatMessage('flow.view')} + + + + {formatMessage('flow.delete')} + + + ); +}; diff --git a/packages/web/src/components/FlowRow/index.tsx b/packages/web/src/components/FlowRow/index.tsx index b220cb90..30d03e6c 100644 --- a/packages/web/src/components/FlowRow/index.tsx +++ b/packages/web/src/components/FlowRow/index.tsx @@ -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(null); + const [anchorEl, setAnchorEl] = React.useState(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 ( - + <> - + - + - {flow.name} + {flow?.name} - + + + {isUpdated && formatMessage('flow.updatedAt', { datetime: relativeUpdatedAt })} + {!isUpdated && formatMessage('flow.createdAt', { datetime: relativeCreatedAt })} + + - theme.palette.primary.main }} /> + + + - + + {anchorEl && } + ); } \ No newline at end of file diff --git a/packages/web/src/components/FlowStep/index.tsx b/packages/web/src/components/FlowStep/index.tsx index 185c22b4..8b2c35cb 100644 --- a/packages/web/src/components/FlowStep/index.tsx +++ b/packages/web/src/components/FlowStep/index.tsx @@ -106,7 +106,7 @@ export default function FlowStep( ); const isTrigger = step.type === 'trigger'; const formatMessage = useFormatMessage(); - const [currentSubstep, setCurrentSubstep] = React.useState(2); + const [currentSubstep, setCurrentSubstep] = React.useState(0); const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }, }); diff --git a/packages/web/src/graphql/mutations/delete-flow.ts b/packages/web/src/graphql/mutations/delete-flow.ts new file mode 100644 index 00000000..f708a5b7 --- /dev/null +++ b/packages/web/src/graphql/mutations/delete-flow.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const DELETE_FLOW = gql` + mutation DeleteFlow($input: DeleteFlowInput) { + deleteFlow(input: $input) + } +`; diff --git a/packages/web/src/graphql/queries/get-flows.ts b/packages/web/src/graphql/queries/get-flows.ts index c8d725be..42ded512 100644 --- a/packages/web/src/graphql/queries/get-flows.ts +++ b/packages/web/src/graphql/queries/get-flows.ts @@ -11,6 +11,8 @@ export const GET_FLOWS = gql` node { id name + createdAt + updatedAt } } } diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index 95f334ee..5e75c89f 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -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",