diff --git a/frontend/src/features/workspace/WorkspaceContent.tsx b/frontend/src/features/workspace/WorkspaceContent.tsx index c0d7e7b9d0..557665846a 100644 --- a/frontend/src/features/workspace/WorkspaceContent.tsx +++ b/frontend/src/features/workspace/WorkspaceContent.tsx @@ -137,7 +137,6 @@ export const WorkspaceContent = ({ > diff --git a/frontend/src/features/workspace/components/WorkspaceHeader/WorkspaceEditDropdown.tsx b/frontend/src/features/workspace/components/WorkspaceHeader/WorkspaceEditDropdown.tsx new file mode 100644 index 0000000000..20ac852f8a --- /dev/null +++ b/frontend/src/features/workspace/components/WorkspaceHeader/WorkspaceEditDropdown.tsx @@ -0,0 +1,50 @@ +import { BiDotsHorizontalRounded } from 'react-icons/bi' +import { MenuButton, useDisclosure } from '@chakra-ui/react' + +import IconButton from '~components/IconButton' +import Menu from '~components/Menu' + +import { DeleteWorkspaceModal } from '../WorkspaceModals/DeleteWorkspaceModal' +import { RenameWorkspaceModal } from '../WorkspaceModals/RenameWorkspaceModal' + +export const WorkspaceEditDropdown = (): JSX.Element => { + const renameModal = useDisclosure() + const deleteModal = useDisclosure() + + return ( + <> + + + + + {({ isOpen }) => ( + <> + } + variant="clear" + colorScheme="secondary" + /> + + + Rename workspace + + + Delete workspace + + + + )} + + + ) +} diff --git a/frontend/src/features/workspace/components/WorkspaceHeader/WorkspaceHeader.tsx b/frontend/src/features/workspace/components/WorkspaceHeader/WorkspaceHeader.tsx index c338091afd..9f675c83e9 100644 --- a/frontend/src/features/workspace/components/WorkspaceHeader/WorkspaceHeader.tsx +++ b/frontend/src/features/workspace/components/WorkspaceHeader/WorkspaceHeader.tsx @@ -1,29 +1,24 @@ import { useState } from 'react' import { BiPlus } from 'react-icons/bi' -import { Skeleton, Stack, Text } from '@chakra-ui/react' +import { Flex, Stack, Text } from '@chakra-ui/react' import { useIsMobile } from '~hooks/useIsMobile' import Button from '~components/Button' import { SortOption } from '~features/workspace/types' +import { WorkspaceEditDropdown } from './WorkspaceEditDropdown' import { WorkspaceSortDropdown } from './WorkspaceSortDropdown' export interface WorkspaceHeaderProps { - /** - * Number of forms in the workspace. - * Defaults to '---' (to account for loading or error states) - */ - totalFormCount?: number | '---' isLoading: boolean handleOpenCreateFormModal: () => void } /** - * Header for listing number of forms, or updating the sort order of listed forms, etc. + * Header for editing workspace, or updating the sort order of listed forms, etc. */ export const WorkspaceHeader = ({ - totalFormCount = '---', isLoading, handleOpenCreateFormModal, }: WorkspaceHeaderProps): JSX.Element => { @@ -37,15 +32,19 @@ export const WorkspaceHeader = ({ align={{ base: 'flex-start', md: 'center' }} spacing="1rem" > - - All forms ({totalFormCount}) - + + + All forms + + + + = (args) => { + const modalProps = useDisclosure({ defaultIsOpen: true }) + + return ( + console.log('close modal')} + /> + ) +} + +export const CreateWorkspace = Template.bind({}) + +export const CreateWorkspaceMobile = Template.bind({}) +CreateWorkspaceMobile.parameters = getMobileViewParameters() diff --git a/frontend/src/features/workspace/components/WorkspaceModals/CreateWorkspaceModal.tsx b/frontend/src/features/workspace/components/WorkspaceModals/CreateWorkspaceModal.tsx new file mode 100644 index 0000000000..478e72bc5c --- /dev/null +++ b/frontend/src/features/workspace/components/WorkspaceModals/CreateWorkspaceModal.tsx @@ -0,0 +1,99 @@ +import { useForm } from 'react-hook-form' +import { + FormControl, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + Text, + useBreakpointValue, +} from '@chakra-ui/react' + +import { useIsMobile } from '~hooks/useIsMobile' +import { WORKSPACE_TITLE_VALIDATION_RULES } from '~utils/workspaceValidation' +import Button from '~components/Button' +import FormErrorMessage from '~components/FormControl/FormErrorMessage' +import Input from '~components/Input' + +type CreateWorkspaceInputProps = { + title: string +} + +export interface CreateWorkspaceModalProps { + isOpen: boolean + onClose: () => void +} + +export const CreateWorkspaceModal = ({ + isOpen, + onClose, +}: CreateWorkspaceModalProps): JSX.Element => { + const { + handleSubmit, + formState: { errors }, + register, + } = useForm({ + defaultValues: { + title: '', + }, + }) + const modalSize = useBreakpointValue({ + base: 'mobile', + xs: 'mobile', + md: 'md', + }) + const isMobile = useIsMobile() + + // TODO (hans): Implement create workspace functionality + const handleCreateWorkspace = handleSubmit((data) => { + onClose() + }) + + return ( + + + + Create workspace + + + Workspace name + + + {errors?.title?.message} + + + + + + + + + + + + ) +} diff --git a/frontend/src/features/workspace/components/WorkspaceModals/DeleteWorkspaceModal.stories.tsx b/frontend/src/features/workspace/components/WorkspaceModals/DeleteWorkspaceModal.stories.tsx new file mode 100644 index 0000000000..5763735ade --- /dev/null +++ b/frontend/src/features/workspace/components/WorkspaceModals/DeleteWorkspaceModal.stories.tsx @@ -0,0 +1,38 @@ +import { useDisclosure } from '@chakra-ui/react' +import { Meta, Story } from '@storybook/react' + +import { userHandlers } from '~/mocks/msw/handlers/user' + +import { getMobileViewParameters } from '~utils/storybook' + +import { + DeleteWorkspaceModal, + DeleteWorkspaceModalProps, +} from '../WorkspaceModals/DeleteWorkspaceModal' + +export default { + title: 'Pages/WorkspacePage/DeleteWorkspaceModal', + component: DeleteWorkspaceModal, + parameters: { + layout: 'fullscreen', + // Prevent flaky tests due to modal animating in. + chromatic: { pauseAnimationAtEnd: true }, + msw: userHandlers({ delay: 0 }), + }, +} as Meta + +const Template: Story = (args) => { + const modalProps = useDisclosure({ defaultIsOpen: true }) + + return ( + console.log('close modal')} + /> + ) +} +export const DeleteWorkspace = Template.bind({}) + +export const DeleteWorkspaceMobile = Template.bind({}) +DeleteWorkspaceMobile.parameters = getMobileViewParameters() diff --git a/frontend/src/features/workspace/components/WorkspaceModals/DeleteWorkspaceModal.tsx b/frontend/src/features/workspace/components/WorkspaceModals/DeleteWorkspaceModal.tsx new file mode 100644 index 0000000000..845b7ded4c --- /dev/null +++ b/frontend/src/features/workspace/components/WorkspaceModals/DeleteWorkspaceModal.tsx @@ -0,0 +1,113 @@ +import { useForm } from 'react-hook-form' +import { + FormControl, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + Text, + useBreakpointValue, +} from '@chakra-ui/react' +import { isEmpty } from 'lodash' + +import { useIsMobile } from '~hooks/useIsMobile' +import Button from '~components/Button' +import FormErrorMessage from '~components/FormControl/FormErrorMessage' +import Radio from '~components/Radio' + +export interface DeleteWorkspaceModalProps { + isOpen: boolean + onClose: () => void +} + +const DELETE_OPTIONS = [ + 'Delete Workspace only', + 'Delete Workspace and all forms within', +] + +export const DeleteWorkspaceModal = ({ + isOpen, + onClose, +}: DeleteWorkspaceModalProps): JSX.Element => { + const { + handleSubmit, + formState: { errors }, + register, + } = useForm() + const modalSize = useBreakpointValue({ + base: 'mobile', + xs: 'mobile', + md: 'md', + }) + const isMobile = useIsMobile() + + // TODO (hans): Implement delete workspace functionality + const handleDeleteWorkspace = handleSubmit((data) => { + onClose() + }) + + return ( + + + + Delete workspace + + + + All responses associated to the forms or workspaces will be deleted. + + + + {DELETE_OPTIONS.map((o, idx) => ( + + {o} + + ))} + + {errors['radio']?.message} + + + + + + + + + + + + ) +} diff --git a/frontend/src/features/workspace/components/WorkspaceModals/RenameWorkspaceModal.stories.tsx b/frontend/src/features/workspace/components/WorkspaceModals/RenameWorkspaceModal.stories.tsx new file mode 100644 index 0000000000..e6a5161eee --- /dev/null +++ b/frontend/src/features/workspace/components/WorkspaceModals/RenameWorkspaceModal.stories.tsx @@ -0,0 +1,38 @@ +import { useDisclosure } from '@chakra-ui/react' +import { Meta, Story } from '@storybook/react' + +import { userHandlers } from '~/mocks/msw/handlers/user' + +import { getMobileViewParameters } from '~utils/storybook' + +import { + RenameWorkspaceModal, + RenameWorkspaceModalProps, +} from './RenameWorkspaceModal' + +export default { + title: 'Pages/WorkspacePage/RenameWorkspaceModal', + component: RenameWorkspaceModal, + parameters: { + layout: 'fullscreen', + // Prevent flaky tests due to modal animating in. + chromatic: { pauseAnimationAtEnd: true }, + msw: userHandlers({ delay: 0 }), + }, +} as Meta + +const Template: Story = (args) => { + const modalProps = useDisclosure({ defaultIsOpen: true }) + + return ( + console.log('close modal')} + /> + ) +} +export const RenameWorkspace = Template.bind({}) + +export const RenameWorkspaceMobile = Template.bind({}) +RenameWorkspaceMobile.parameters = getMobileViewParameters() diff --git a/frontend/src/features/workspace/components/WorkspaceModals/RenameWorkspaceModal.tsx b/frontend/src/features/workspace/components/WorkspaceModals/RenameWorkspaceModal.tsx new file mode 100644 index 0000000000..1cdf00370a --- /dev/null +++ b/frontend/src/features/workspace/components/WorkspaceModals/RenameWorkspaceModal.tsx @@ -0,0 +1,99 @@ +import { useForm } from 'react-hook-form' +import { + FormControl, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Stack, + Text, + useBreakpointValue, +} from '@chakra-ui/react' + +import { useIsMobile } from '~hooks/useIsMobile' +import { WORKSPACE_TITLE_VALIDATION_RULES } from '~utils/workspaceValidation' +import Button from '~components/Button' +import FormErrorMessage from '~components/FormControl/FormErrorMessage' +import Input from '~components/Input' + +type RenameWorkspaceInputProps = { + title: string +} + +export interface RenameWorkspaceModalProps { + isOpen: boolean + onClose: () => void +} + +export const RenameWorkspaceModal = ({ + isOpen, + onClose, +}: RenameWorkspaceModalProps): JSX.Element => { + const { + handleSubmit, + formState: { errors }, + register, + } = useForm({ + defaultValues: { + title: '', + }, + }) + const modalSize = useBreakpointValue({ + base: 'mobile', + xs: 'mobile', + md: 'md', + }) + const isMobile = useIsMobile() + + // TODO (hans): Implement rename workspace functionality + const handleRenameWorkspace = handleSubmit((data) => { + onClose() + }) + + return ( + + + + Rename workspace + + + Workspace name + + + {errors?.title?.message} + + + + + + + + + + + + ) +} diff --git a/frontend/src/utils/workspaceValidation.ts b/frontend/src/utils/workspaceValidation.ts new file mode 100644 index 0000000000..ea93f3aa9c --- /dev/null +++ b/frontend/src/utils/workspaceValidation.ts @@ -0,0 +1,24 @@ +import { UseControllerProps } from 'react-hook-form' + +const MAX_TITLE_LENGTH = 50 +const MIN_TITLE_LENGTH = 4 + +export const WORKSPACE_TITLE_VALIDATION_RULES: UseControllerProps['rules'] = { + required: 'Workspace title is required', + maxLength: { + value: MAX_TITLE_LENGTH, + message: `Workspace title must be at most ${MAX_TITLE_LENGTH} characters`, + }, + pattern: { + value: /^[a-zA-Z0-9_\-./() &`;'"]*$/, + message: 'Workspace title cannot contain special characters', + }, + validate: { + trimMinLength: (value: string) => { + return ( + value.trim().length >= MIN_TITLE_LENGTH || + `Workspace title must be at least ${MIN_TITLE_LENGTH} characters` + ) + }, + }, +}