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 (
+ <>
+
+
+
+
+ >
+ )
+}
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`
+ )
+ },
+ },
+}