diff --git a/packages/web/src/components/ConfirmationDialog/index.jsx b/packages/web/src/components/ConfirmationDialog/index.jsx index 61cff3ae..522da042 100644 --- a/packages/web/src/components/ConfirmationDialog/index.jsx +++ b/packages/web/src/components/ConfirmationDialog/index.jsx @@ -19,7 +19,9 @@ function ConfirmationDialog(props) { open = true, errorMessage, } = props; + const dataTest = props['data-test']; + return ( {title && {title}} @@ -46,6 +48,7 @@ function ConfirmationDialog(props) { )} + {errorMessage && ( {errorMessage} diff --git a/packages/web/src/components/CreateFolderDialog/index.jsx b/packages/web/src/components/CreateFolderDialog/index.jsx new file mode 100644 index 00000000..31730243 --- /dev/null +++ b/packages/web/src/components/CreateFolderDialog/index.jsx @@ -0,0 +1,106 @@ +import AddIcon from '@mui/icons-material/Add'; +import CloseIcon from '@mui/icons-material/Close'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import * as React from 'react'; + +import { getUnifiedErrorMessage } from 'helpers/errors'; +import useCreateFolder from 'hooks/useCreateFolder'; +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function CreateFolderDialog(props) { + const { open = true, onClose } = props; + + const [folderName, setFolderName] = React.useState(''); + const formatMessage = useFormatMessage(); + + const { mutate: createFolder, error, isError, isSuccess } = useCreateFolder(); + + const handleCreateFolder = () => { + createFolder({ name: folderName }); + + setFolderName(''); + }; + + const handleTextFieldChange = (event) => { + setFolderName(event.target.value); + }; + + const handleTextFieldKeyDown = (event) => { + if (event.key === 'Enter') { + handleCreateFolder(); + } + }; + + return ( + + {formatMessage('createFolderDialog.title')} + + theme.palette.grey[500], + }} + > + + + + + + {formatMessage('createFolderDialog.description')} + + + + + + + + + + {isError && ( + + {getUnifiedErrorMessage(error?.response?.data?.errors) || + formatMessage('genericError')} + + )} + + {isSuccess && ( + + {formatMessage('createFolderDialog.successfullyCreatedFolder')} + + )} + + ); +} diff --git a/packages/web/src/components/EditFolderDialog/index.jsx b/packages/web/src/components/EditFolderDialog/index.jsx new file mode 100644 index 00000000..e9027560 --- /dev/null +++ b/packages/web/src/components/EditFolderDialog/index.jsx @@ -0,0 +1,107 @@ +import CloseIcon from '@mui/icons-material/Close'; +import SaveIcon from '@mui/icons-material/Save'; +import Alert from '@mui/material/Alert'; +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import * as React from 'react'; + +import { getUnifiedErrorMessage } from 'helpers/errors'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useUpdateFolder from 'hooks/useUpdateFolder'; + +export default function EditFolderDialog(props) { + const { open = true, onClose, folder = {} } = props; + + const [folderName, setFolderName] = React.useState(folder.name); + const formatMessage = useFormatMessage(); + + const { + mutate: updateFolder, + error, + isError, + isSuccess, + } = useUpdateFolder(folder.id); + + const handleUpdateFolder = () => { + updateFolder({ name: folderName }); + }; + + const handleTextFieldChange = (event) => { + setFolderName(event.target.value); + }; + + const handleTextFieldKeyDown = (event) => { + if (event.key === 'Enter') { + handleUpdateFolder(); + } + }; + + return ( + + {formatMessage('editFolderDialog.title')} + + theme.palette.grey[500], + }} + > + + + + + + {formatMessage('editFolderDialog.description')} + + + + + + + + + + {isError && ( + + {getUnifiedErrorMessage(error?.response?.data?.errors) || + formatMessage('genericError')} + + )} + + {isSuccess && ( + + {formatMessage('editFolderDialog.successfullyUpdatedFolder')} + + )} + + ); +} diff --git a/packages/web/src/components/EditorLayout/FlowFolder.jsx b/packages/web/src/components/EditorLayout/FlowFolder.jsx new file mode 100644 index 00000000..264d1b0f --- /dev/null +++ b/packages/web/src/components/EditorLayout/FlowFolder.jsx @@ -0,0 +1,34 @@ +import { Link, useParams } from 'react-router-dom'; +import CircularProgress from '@mui/material/CircularProgress'; +import * as React from 'react'; +import Typography from '@mui/material/Typography'; + +import * as URLS from 'config/urls'; +import useFlowFolder from 'hooks/useFlowFolder'; +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function FlowFolder(props) { + const { flowId } = props; + + const formatMessage = useFormatMessage(); + const { data: folder, isLoading, isError } = useFlowFolder(flowId); + + const name = folder?.data?.name || formatMessage('flowFolder.uncategorized'); + const id = folder?.data?.id || 'null'; + + return ( + + {!isLoading && name} + {isLoading && ( + + )} + + ); +} diff --git a/packages/web/src/components/EditorLayout/index.jsx b/packages/web/src/components/EditorLayout/index.jsx index 201b6407..3ca0e35c 100644 --- a/packages/web/src/components/EditorLayout/index.jsx +++ b/packages/web/src/components/EditorLayout/index.jsx @@ -1,30 +1,32 @@ -import * as React from 'react'; -import { Link, useParams } from 'react-router-dom'; -import Stack from '@mui/material/Stack'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Tooltip from '@mui/material/Tooltip'; -import IconButton from '@mui/material/IconButton'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import DownloadIcon from '@mui/icons-material/Download'; +import Box from '@mui/material/Box'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; import Snackbar from '@mui/material/Snackbar'; +import Stack from '@mui/material/Stack'; +import Tooltip from '@mui/material/Tooltip'; +import * as React from 'react'; +import { Link, useParams } from 'react-router-dom'; import { ReactFlowProvider } from 'reactflow'; -import { EditorProvider } from 'contexts/Editor'; -import { TopBar } from './style'; -import * as URLS from 'config/urls'; import Can from 'components/Can'; import Container from 'components/Container'; import EditableTypography from 'components/EditableTypography'; import Editor from 'components/Editor'; import EditorNew from 'components/EditorNew/EditorNew'; +import * as URLS from 'config/urls'; +import { EditorProvider } from 'contexts/Editor'; +import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import useExportFlow from 'hooks/useExportFlow'; import useFlow from 'hooks/useFlow'; import useFormatMessage from 'hooks/useFormatMessage'; import useUpdateFlow from 'hooks/useUpdateFlow'; import useUpdateFlowStatus from 'hooks/useUpdateFlowStatus'; -import useExportFlow from 'hooks/useExportFlow'; -import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile'; -import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import FlowFolder from './FlowFolder'; +import { TopBar } from './style'; const useNewFlowEditor = process.env.REACT_APP_USE_NEW_FLOW_EDITOR === 'true'; @@ -70,7 +72,7 @@ export default function EditorLayout() { px={1} className="mui-fixed" > - + - {!isFlowLoading && ( - - {flow?.name} - - )} + + + + {!isFlowLoading && ( + + {flow?.name} + + )} + diff --git a/packages/web/src/components/FlowContextMenu/index.jsx b/packages/web/src/components/FlowContextMenu/index.jsx index 2d4d8263..5b7ce68e 100644 --- a/packages/web/src/components/FlowContextMenu/index.jsx +++ b/packages/web/src/components/FlowContextMenu/index.jsx @@ -1,22 +1,25 @@ -import PropTypes from 'prop-types'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import { useQueryClient } from '@tanstack/react-query'; +import PropTypes from 'prop-types'; import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; import * as React from 'react'; import { Link } from 'react-router-dom'; import Can from 'components/Can'; +import FlowFolderChangeDialog from 'components/FlowFolderChangeDialog'; import * as URLS from 'config/urls'; -import useFormatMessage from 'hooks/useFormatMessage'; -import useDuplicateFlow from 'hooks/useDuplicateFlow'; import useDeleteFlow from 'hooks/useDeleteFlow'; -import useExportFlow from 'hooks/useExportFlow'; import useDownloadJsonAsFile from 'hooks/useDownloadJsonAsFile'; +import useDuplicateFlow from 'hooks/useDuplicateFlow'; +import useExportFlow from 'hooks/useExportFlow'; +import useFormatMessage from 'hooks/useFormatMessage'; function ContextMenu(props) { const { flowId, onClose, anchorEl, onDuplicateFlow, appKey } = props; + const [showFlowFolderChangeDialog, setShowFlowFolderChangeDialog] = + React.useState(false); const enqueueSnackbar = useEnqueueSnackbar(); const formatMessage = useFormatMessage(); const queryClient = useQueryClient(); @@ -91,45 +94,67 @@ function ContextMenu(props) { onClose(); }, [exportFlow, downloadJsonAsFile, enqueueSnackbar, formatMessage, onClose]); + const onFlowFolderUpdate = React.useCallback(() => { + setShowFlowFolderChangeDialog(true); + }, []); + return ( - - - {(allowed) => ( - - {formatMessage('flow.view')} - - )} - + <> + + + {(allowed) => ( + + {formatMessage('flow.view')} + + )} + - - {(allowed) => ( - - {formatMessage('flow.duplicate')} - - )} - + + {(allowed) => ( + + {formatMessage('flow.duplicate')} + + )} + - - {(allowed) => ( - - {formatMessage('flow.export')} - - )} - + + {(allowed) => ( + + {formatMessage('flow.moveTo')} + + )} + - - {(allowed) => ( - - {formatMessage('flow.delete')} - - )} - - + + {(allowed) => ( + + {formatMessage('flow.export')} + + )} + + + + {(allowed) => ( + + {formatMessage('flow.delete')} + + )} + + + + {showFlowFolderChangeDialog && ( + + )} + ); } diff --git a/packages/web/src/components/FlowFolderChangeDialog/index.jsx b/packages/web/src/components/FlowFolderChangeDialog/index.jsx new file mode 100644 index 00000000..bdbc3fe0 --- /dev/null +++ b/packages/web/src/components/FlowFolderChangeDialog/index.jsx @@ -0,0 +1,145 @@ +import Alert from '@mui/material/Alert'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import LoadingButton from '@mui/lab/LoadingButton'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import FormControl from '@mui/material/FormControl'; +import CircularProgress from '@mui/material/CircularProgress'; +import Autocomplete from '@mui/material/Autocomplete'; +import TextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import CloseIcon from '@mui/icons-material/Close'; +import * as React from 'react'; + +import useFolders from 'hooks/useFolders'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useFlowFolder from 'hooks/useFlowFolder'; +import useUpdateFlowFolder from 'hooks/useUpdateFlowFolder'; + +function FlowFolderChangeDialog(props) { + const { flowId, onClose, open = true } = props; + + const formatMessage = useFormatMessage(); + const { data: folders, isLoading: isFoldersLoading } = useFolders(); + const { data: flowFolder, isLoading: isFlowFolderLoading } = + useFlowFolder(flowId); + + const [selectedFolder, setSelectedFolder] = React.useState(null); + + const uncategorizedFolder = { id: null, name: 'Uncategorized' }; + + const { + mutate: updateFlowFolder, + isSuccess, + isPending: isUpdateFlowFolderPending, + error: createUpdateFlowFolderError, + } = useUpdateFlowFolder(flowId); + + const handleChange = (event, newValue) => { + setSelectedFolder(newValue ? newValue.id : null); + }; + + const handleConfirm = () => { + updateFlowFolder(selectedFolder); + }; + + React.useEffect( + function updateInitialSelectedFolder() { + if (!flowFolder) return; + + setSelectedFolder(flowFolder.data?.id || null); + }, + [flowFolder], + ); + + return ( + + {formatMessage('flowFolderChangeDialog.title')} + + theme.palette.grey[500], + }} + > + + + + + + {formatMessage('flowFolderChangeDialog.description')} + + + + folder.id === selectedFolder) || + uncategorizedFolder + } + disableClearable={true} + onChange={handleChange} + options={[uncategorizedFolder, ...(folders?.data || [])]} + isOptionEqualToValue={(option, value) => option.id === value.id} + getOptionLabel={(option) => option.name} + loading={isFoldersLoading || isFlowFolderLoading} + disabled={isFoldersLoading || isFlowFolderLoading} + renderInput={(params) => ( + + {(isFoldersLoading || isFlowFolderLoading) && ( + + )} + + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + + + + {formatMessage('flowFolderChangeDialog.confirm')} + + + + {createUpdateFlowFolderError && ( + + {createUpdateFlowFolderError.message} + + )} + + {isSuccess && ( + + {formatMessage('flowFolderChangeDialog.successfullyUpdatedFolder')} + + )} + + ); +} + +export default FlowFolderChangeDialog; diff --git a/packages/web/src/components/FlowFolderChangeDialog/style.js b/packages/web/src/components/FlowFolderChangeDialog/style.js new file mode 100644 index 00000000..76d349ee --- /dev/null +++ b/packages/web/src/components/FlowFolderChangeDialog/style.js @@ -0,0 +1,6 @@ +import { styled } from '@mui/material/styles'; +import MuiListItemIcon from '@mui/material/ListItemIcon'; + +export const ListItemIcon = styled(MuiListItemIcon)(({ theme }) => ({ + minWidth: theme.spacing(4), +})); diff --git a/packages/web/src/components/FlowRow/index.jsx b/packages/web/src/components/FlowRow/index.jsx index 9b2e98f3..0288cc15 100644 --- a/packages/web/src/components/FlowRow/index.jsx +++ b/packages/web/src/components/FlowRow/index.jsx @@ -116,6 +116,7 @@ function FlowRow(props) { {anchorEl && ( folder.id === selectedFolderId, + ); + + const allFlowsFolder = new URLSearchParams().toString(); + const unassignedFlowsFolder = new URLSearchParams('folderId=null').toString(); + + const allFlowsFolderSelected = selectedFolderId === null; + const unassignedFlowsFolderSelected = selectedFolderId === 'null'; // intendedly stringified + + const generalErrorMessage = getGeneralErrorMessage({ + error: deleteFolderError, + fallbackMessage: formatMessage('genericError'), + }); + + const handleDeleteFolderConfirmation = async () => { + await deleteFolder(selectedFolderId); + + navigate({ search: allFlowsFolder }); + }; + + const getFolderSearchParams = (folderId) => { + const searchParams = new URLSearchParams(`folderId=${folderId}`); + + return searchParams.toString(); + }; + + const generateFolderItem = (folder) => { + if (folder.id === selectedFolderId) { + return generateListItem(folder); + } + + return generateListItemButton(folder); + }; + + const generateListItem = (folder) => { + return ( + + setShowEditFolderDialog(true)} + > + + + setShowDeleteFolderDialog(true)} + > + + + + } + > + + + + + ); + }; + + const generateListItemButton = (folder) => { + return ( + + + + ); + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + {folders?.data?.map((folder) => generateFolderItem(folder))} + + setShowCreateFolderDialog(true)}> + + + + + + + + + + {showCreateFolderDialog && ( + setShowCreateFolderDialog(false)} /> + )} + + {selectedFolder && showEditFolderDialog && ( + setShowEditFolderDialog(false)} + /> + )} + + {selectedFolder && showDeleteFolderDialog && ( + setShowDeleteFolderDialog(false)} + onConfirm={handleDeleteFolderConfirmation} + cancelButtonChildren={formatMessage('deleteFolderDialog.cancel')} + confirmButtonChildren={formatMessage('deleteFolderDialog.confirm')} + errorMessage={generalErrorMessage} + /> + )} + + ); +} diff --git a/packages/web/src/components/Folders/style.js b/packages/web/src/components/Folders/style.js new file mode 100644 index 00000000..76d349ee --- /dev/null +++ b/packages/web/src/components/Folders/style.js @@ -0,0 +1,6 @@ +import { styled } from '@mui/material/styles'; +import MuiListItemIcon from '@mui/material/ListItemIcon'; + +export const ListItemIcon = styled(MuiListItemIcon)(({ theme }) => ({ + minWidth: theme.spacing(4), +})); diff --git a/packages/web/src/config/urls.js b/packages/web/src/config/urls.js index 5c9d4f72..ceba4489 100644 --- a/packages/web/src/config/urls.js +++ b/packages/web/src/config/urls.js @@ -47,6 +47,7 @@ export const APP_FLOWS_FOR_CONNECTION = (appKey, connectionId) => `/app/${appKey}/flows?connectionId=${connectionId}`; export const APP_FLOWS = (appKey) => `/app/${appKey}/flows`; +export const FOLDER_FLOWS = (folderId) => `/flows?folderId=${folderId}`; export const APP_FLOWS_PATTERN = '/app/:appKey/flows'; export const EDITOR = '/editor'; export const CREATE_FLOW = '/editor/create'; diff --git a/packages/web/src/hooks/useCreateFolder.js b/packages/web/src/hooks/useCreateFolder.js new file mode 100644 index 00000000..8f951caf --- /dev/null +++ b/packages/web/src/hooks/useCreateFolder.js @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useCreateFolder() { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (payload) => { + const { data } = await api.post('/v1/folders', payload); + + return data; + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['folders'], + }); + }, + }); + + return mutation; +} diff --git a/packages/web/src/hooks/useDeleteFolder.js b/packages/web/src/hooks/useDeleteFolder.js new file mode 100644 index 00000000..5a30d535 --- /dev/null +++ b/packages/web/src/hooks/useDeleteFolder.js @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useDeleteFolder() { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (id) => { + const { data } = await api.delete(`/v1/folders/${id}`); + + return data; + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['folders'], + }); + }, + }); + + return mutation; +} diff --git a/packages/web/src/hooks/useFlowFolder.js b/packages/web/src/hooks/useFlowFolder.js new file mode 100644 index 00000000..e3f20bae --- /dev/null +++ b/packages/web/src/hooks/useFlowFolder.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useFlowFolder(flowId) { + const query = useQuery({ + queryKey: ['flows', flowId, 'folder'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/flows/${flowId}/folder`, { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useFlows.js b/packages/web/src/hooks/useFlows.js index 3b237cf6..97d9a8a3 100644 --- a/packages/web/src/hooks/useFlows.js +++ b/packages/web/src/hooks/useFlows.js @@ -1,12 +1,12 @@ import api from 'helpers/api'; import { useQuery } from '@tanstack/react-query'; -export default function useFlows({ flowName, page }) { +export default function useFlows({ flowName, page, folderId }) { const query = useQuery({ - queryKey: ['flows', flowName, { page }], + queryKey: ['flows', flowName, { page, folderId }], queryFn: async ({ signal }) => { const { data } = await api.get('/v1/flows', { - params: { name: flowName, page }, + params: { name: flowName, page, folderId }, signal, }); diff --git a/packages/web/src/hooks/useFolders.js b/packages/web/src/hooks/useFolders.js new file mode 100644 index 00000000..7b295217 --- /dev/null +++ b/packages/web/src/hooks/useFolders.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useFolders() { + const query = useQuery({ + queryKey: ['folders'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/folders', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useUpdateFlowFolder.js b/packages/web/src/hooks/useUpdateFlowFolder.js new file mode 100644 index 00000000..eaa45766 --- /dev/null +++ b/packages/web/src/hooks/useUpdateFlowFolder.js @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useUpdateFlowFolder(flowId) { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (folderId) => { + const { data } = await api.patch(`/v1/flows/${flowId}/folder`, { + folderId, + }); + + return data; + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['flows', flowId, 'folder'], + }); + }, + }); + + return mutation; +} diff --git a/packages/web/src/hooks/useUpdateFolder.js b/packages/web/src/hooks/useUpdateFolder.js new file mode 100644 index 00000000..197fd26d --- /dev/null +++ b/packages/web/src/hooks/useUpdateFolder.js @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useUpdateFolder(folderId) { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (payload) => { + const { data } = await api.patch(`/v1/folders/${folderId}`, payload); + + return data; + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['folders'], + }); + }, + }); + + return mutation; +} diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json index f24de311..5fd4aff2 100644 --- a/packages/web/src/locales/en.json +++ b/packages/web/src/locales/en.json @@ -86,6 +86,7 @@ "flow.updatedAt": "updated {datetime}", "flow.view": "View", "flow.duplicate": "Duplicate", + "flow.moveTo": "Move to", "flow.delete": "Delete", "flow.export": "Export", "flowStep.triggerType": "Trigger", @@ -329,5 +330,29 @@ "importFlowDialog.close": "Close", "importFlowDialog.import": "Import", "importFlowDialog.selectedFileInformation": "Selected file: {fileName}", - "importFlowDialog.successfullyImportedFlow": "The flow has been successfully imported. You can view it here." + "importFlowDialog.successfullyImportedFlow": "The flow has been successfully imported. You can view it here.", + "folders.allFlows": "All flows", + "folders.unassignedFlows": "Uncategorized flows", + "folders.createNew": "New folder", + "createFolderDialog.title": "Create folder", + "createFolderDialog.create": "Create", + "createFolderDialog.description": "You can create a new folder by submitting the form below.", + "createFolderDialog.folderNameInputLabel": "Folder name", + "createFolderDialog.successfullyCreatedFolder": "The folder has been successfully created!", + "editFolderDialog.title": "Update folder", + "editFolderDialog.update": "Update", + "editFolderDialog.description": "You can update your folder by submitting the form below.", + "editFolderDialog.folderNameInputLabel": "New folder name", + "editFolderDialog.successfullyUpdatedFolder": "The folder has been successfully updated!", + "deleteFolderDialog.title": "Delete folder", + "deleteFolderDialog.description": "This will permanently delete the folder and move the flows into uncategorized folder", + "deleteFolderDialog.cancel": "Cancel", + "deleteFolderDialog.confirm": "Yes, delete it", + "flowFolderChangeDialog.title": "Move to", + "flowFolderChangeDialog.folderInputLabel": "Folder", + "flowFolderChangeDialog.description": "You can change the folder of the flow by submitting the form below.", + "flowFolderChangeDialog.confirm": "Move", + "flowFolderChangeDialog.uncategorizedFolder": "Uncategorized", + "flowFolderChangeDialog.successfullyUpdatedFolder": "The flow has been successfully moved to the new folder!", + "flowFolder.uncategorized": "Uncategorized" } diff --git a/packages/web/src/pages/Flows/index.jsx b/packages/web/src/pages/Flows/index.jsx index 92d3308e..90aa5a94 100644 --- a/packages/web/src/pages/Flows/index.jsx +++ b/packages/web/src/pages/Flows/index.jsx @@ -16,6 +16,7 @@ import Pagination from '@mui/material/Pagination'; import PaginationItem from '@mui/material/PaginationItem'; import Can from 'components/Can'; +import Folders from 'components/Folders'; import FlowRow from 'components/FlowRow'; import NoResultFound from 'components/NoResultFound'; import ConditionalIconButton from 'components/ConditionalIconButton'; @@ -34,9 +35,10 @@ export default function Flows() { const [searchParams, setSearchParams] = useSearchParams(); const page = parseInt(searchParams.get('page') || '', 10) || 1; const flowName = searchParams.get('flowName') || ''; + const folderId = searchParams.get('folderId'); const currentUserAbility = useCurrentUserAbility(); - const { data, isSuccess, isLoading } = useFlows({ flowName, page }); + const { data, isSuccess, isLoading } = useFlows({ flowName, page, folderId }); const flows = data?.data || []; const pageInfo = data?.meta; @@ -50,13 +52,18 @@ export default function Flows() { const getPathWithSearchParams = (page, flowName) => { const searchParams = new URLSearchParams(); - if (page > 1) { - searchParams.set('page', page); + if (folderId) { + searchParams.set('folderId', folderId); } + if (flowName) { searchParams.set('flowName', flowName); } + if (page > 1) { + searchParams.set('page', page); + } + return { search: searchParams.toString() }; }; @@ -144,45 +151,55 @@ export default function Flows() { - {(isLoading || navigateToLastPage) && ( - - )} + + + + - {!isLoading && - flows?.map((flow) => ( - - ))} + + {(isLoading || navigateToLastPage) && ( + + )} - {!isLoading && !navigateToLastPage && !hasFlows && ( - - )} + {!isLoading && + flows?.map((flow) => ( + + ))} - {!isLoading && - !navigateToLastPage && - pageInfo && - pageInfo.totalPages > 1 && ( - ( - + )} + + {!isLoading && + !navigateToLastPage && + pageInfo && + pageInfo.totalPages > 1 && ( + ( + + )} /> )} - /> - )} + +