Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(builder/0): add initial form builder layout #3126

Merged
merged 8 commits into from
Jan 13, 2022
3 changes: 2 additions & 1 deletion frontend/src/app/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

import { AdminFormLayout } from '~features/admin-form/common/AdminFormLayout'
import { SettingsPage } from '~features/admin-form/settings/SettingsPage'
import { FormBuilderPage } from '~features/admin-form-builder/FormBuilderPage'
import { PublicFormPage } from '~features/public-form/PublicFormPage'

import { PrivateElement } from './PrivateElement'
Expand Down Expand Up @@ -44,7 +45,7 @@ export const AppRouter = (): JSX.Element => {
path={`${ADMINFORM_ROUTE}/:formId`}
element={<PrivateElement element={<AdminFormLayout />} />}
>
<Route index element={<div>Builder subpage</div>} />
<Route index element={<FormBuilderPage />} />
<Route
path={ADMINFORM_SETTINGS_SUBROUTE}
element={<SettingsPage />}
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/assets/icons/BxsColorFill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const BxsColorFill = (
props: React.SVGProps<SVGSVGElement>,
): JSX.Element => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="currentColor"
{...props}
>
<path d="M20 14c-.092.064-2 2.083-2 3.5 0 1.494.949 2.448 2 2.5.906.044 2-.891 2-2.5 0-1.5-1.908-3.436-2-3.5zM9.586 20c.378.378.88.586 1.414.586s1.036-.208 1.414-.586l7-7-.707-.707L11 4.586 8.707 2.293 7.293 3.707 9.586 6 4 11.586c-.378.378-.586.88-.586 1.414s.208 1.036.586 1.414L9.586 20zM11 7.414L16.586 13H5.414L11 7.414z" />
</svg>
)
}
16 changes: 16 additions & 0 deletions frontend/src/assets/icons/BxsWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const BxsWidget = (
props: React.SVGProps<SVGSVGElement>,
): JSX.Element => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
height="1em"
width="1em"
{...props}
>
<path d="M4 11h6a1 1 0 001-1V4a1 1 0 00-1-1H4a1 1 0 00-1 1v6a1 1 0 001 1zm0 10h6a1 1 0 001-1v-6a1 1 0 00-1-1H4a1 1 0 00-1 1v6a1 1 0 001 1zm10 0h6a1 1 0 001-1v-6a1 1 0 00-1-1h-6a1 1 0 00-1 1v6a1 1 0 001 1zm7.293-14.707l-3.586-3.586a.999.999 0 00-1.414 0l-3.586 3.586a.999.999 0 000 1.414l3.586 3.586a.999.999 0 001.414 0l3.586-3.586a.999.999 0 000-1.414z" />
</svg>
)
}
7 changes: 7 additions & 0 deletions frontend/src/components/motion/MotionBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FC } from 'react'
import { Box, BoxProps } from '@chakra-ui/react'
import { HTMLMotionProps, motion } from 'framer-motion'
import { Merge } from 'type-fest'

export type MotionBoxProps = Merge<BoxProps, HTMLMotionProps<'div'>>
export const MotionBox: FC<MotionBoxProps> = motion(Box)
2 changes: 2 additions & 0 deletions frontend/src/components/motion/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { MotionBoxProps } from './MotionBox'
export { MotionBox } from './MotionBox'
50 changes: 50 additions & 0 deletions frontend/src/features/admin-form-builder/BuilderContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Flex, Stack } from '@chakra-ui/react'

import { useAdminForm } from '~features/admin-form/common/queries'

import { FieldRowContainer } from './FieldRow/FieldRowContainer'

export const BuilderContent = (): JSX.Element => {
return (
<Flex flex={1} bg="neutral.200">
<Flex
m="2rem"
mb={0}
flex={1}
bg="primary.100"
p="2.5rem"
justify="center"
overflow="auto"
>
<Flex
h="fit-content"
bg="white"
p="2.5rem"
maxW="57rem"
w="100%"
flexDir="column"
>
<Stack spacing="2.25rem">
<BuilderFields />
</Stack>
</Flex>
</Flex>
</Flex>
)
}

export const BuilderFields = () => {
const { data, isLoading } = useAdminForm()

if (!data || isLoading) {
return <div>Loading...</div>
}

return (
<>
{data.form_fields.map((f) => (
<FieldRowContainer isActive={false} field={f} />
))}
</>
)
}
48 changes: 48 additions & 0 deletions frontend/src/features/admin-form-builder/BuilderDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Box } from '@chakra-ui/react'
import { AnimatePresence } from 'framer-motion'

import { MotionBox } from '~components/motion'

import { useBuilderDrawer } from './BuilderDrawerContext'

const DRAWER_MOTION_PROPS = {
initial: { width: 0 },
animate: {
maxWidth: '33.25rem',
width: '36%',
transition: {
bounce: 0,
duration: 0.2,
},
},
exit: {
width: 0,
opacity: 0,
transition: {
duration: 0.2,
},
},
}

export const BuilderDrawer = (): JSX.Element => {
const { isShowDrawer } = useBuilderDrawer()

return (
<AnimatePresence>
{isShowDrawer && (
<MotionBox
bg="white"
key="sidebar"
pos="relative"
as="aside"
overflow="auto"
{...DRAWER_MOTION_PROPS}
>
<Box w="100%" h="100%" minW="max-content">
Drawer stuff
</Box>
</MotionBox>
)}
</AnimatePresence>
)
}
78 changes: 78 additions & 0 deletions frontend/src/features/admin-form-builder/BuilderDrawerContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
createContext,
FC,
useCallback,
useContext,
useMemo,
useState,
} from 'react'

export enum DrawerTabs {
Builder,
Design,
Logic,
}

type BuilderDrawerContextProps = {
activeTab: DrawerTabs | null
isShowDrawer: boolean
handleClose: () => void
handleBuilderClick: () => void
handleDesignClick: () => void
handleLogicClick: () => void
}

const BuilderDrawerContext = createContext<
BuilderDrawerContextProps | undefined
>(undefined)

/**
* Provider component that makes drawer context object available to any
* child component that calls `useBuilderDrawer()`.
*/
export const BuilderDrawerProvider: FC = ({ children }) => {
const context = useProvideDrawerContext()

return (
<BuilderDrawerContext.Provider value={context}>
{children}
</BuilderDrawerContext.Provider>
)
}

/**
* Hook for components nested in ProvideAuth component to get the current auth object.
*/
export const useBuilderDrawer = (): BuilderDrawerContextProps => {
const context = useContext(BuilderDrawerContext)
if (!context) {
throw new Error(
`useBuilderDrawer must be used within a BuilderDrawerProvider component`,
)
}
return context
}

const useProvideDrawerContext = (): BuilderDrawerContextProps => {
const [activeTab, setActiveTab] = useState<DrawerTabs | null>(null)

const isShowDrawer = useMemo(
() => activeTab !== null && activeTab !== DrawerTabs.Logic,
[activeTab],
)

const handleClose = useCallback(() => setActiveTab(null), [])

const handleBuilderClick = () => setActiveTab(DrawerTabs.Builder)
const handleDesignClick = () => setActiveTab(DrawerTabs.Design)
const handleLogicClick = () => setActiveTab(DrawerTabs.Logic)

return {
activeTab,
isShowDrawer,
handleClose,
handleBuilderClick,
handleDesignClick,
handleLogicClick,
}
}
43 changes: 43 additions & 0 deletions frontend/src/features/admin-form-builder/BuilderSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { BiGitMerge } from 'react-icons/bi'
import { Stack } from '@chakra-ui/react'

import { BxsColorFill } from '~assets/icons/BxsColorFill'
import { BxsWidget } from '~assets/icons/BxsWidget'

import { DrawerTabs, useBuilderDrawer } from './BuilderDrawerContext'
import { DrawerTabIcon } from './DrawerTabIcon'

export const BuilderSidebar = (): JSX.Element => {
const { activeTab, handleBuilderClick, handleDesignClick, handleLogicClick } =
useBuilderDrawer()

return (
<Stack
bg="white"
spacing="0.5rem"
py="1rem"
px="0.5rem"
borderRight="1px solid"
borderColor="neutral.300"
>
<DrawerTabIcon
label="Build your form"
icon={<BxsWidget fontSize="1.5rem" />}
onClick={handleBuilderClick}
isActive={activeTab === DrawerTabs.Builder}
/>
<DrawerTabIcon
label="Design your form"
icon={<BxsColorFill fontSize="1.5rem" />}
onClick={handleDesignClick}
isActive={activeTab === DrawerTabs.Design}
/>
<DrawerTabIcon
label="Add conditional logic"
icon={<BiGitMerge fontSize="1.5rem" />}
onClick={handleLogicClick}
isActive={activeTab === DrawerTabs.Logic}
/>
</Stack>
)
}
27 changes: 27 additions & 0 deletions frontend/src/features/admin-form-builder/DrawerTabIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import IconButton from '~components/IconButton'
import Tooltip from '~components/Tooltip'

interface DrawerTabIconProps {
icon: React.ReactElement
onClick: () => void
label: string
isActive: boolean
}
export const DrawerTabIcon = ({
icon,
onClick,
label,
isActive,
}: DrawerTabIconProps): JSX.Element => {
return (
<Tooltip label={label} placement="right">
<IconButton
variant="reverse"
aria-label={label}
isActive={isActive}
icon={icon}
onClick={onClick}
/>
</Tooltip>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { BiDuplicate, BiGridHorizontal, BiTrash } from 'react-icons/bi'
import { Box, ButtonGroup, Collapse, Flex, Icon } from '@chakra-ui/react'

import { FormFieldDto } from '~shared/types/field'

import IconButton from '~components/IconButton'

export interface FieldRowContainerProps {
isActive: boolean
field: FormFieldDto
}

export const FieldRowContainer = ({
isActive,
field,
}: FieldRowContainerProps): JSX.Element => {
return (
<Flex
transitionDuration="normal"
bg="white"
_hover={{ bg: 'secondary.100' }}
borderRadius="4px"
{...(isActive ? { 'data-active': true } : {})}
_focusWithin={{
boxShadow: '0 0 0 2px var(--chakra-colors-primary-500)',
}}
_active={{
bg: 'secondary.100',
boxShadow: '0 0 0 2px var(--chakra-colors-primary-500)',
}}
flexDir="column"
align="center"
>
<Icon
as={BiGridHorizontal}
color="secondary.200"
fontSize="1.5rem"
cursor="grab"
transition="color 0.2s ease"
_hover={{
color: 'secondary.300',
}}
/>
<Box p="1.5rem" pt={0} w="100%">
TODO: Add field row for {field.fieldType}
</Box>
<Collapse in={isActive} style={{ width: '100%' }}>
<Flex
px="1.5rem"
flex={1}
borderTop="1px solid var(--chakra-colors-neutral-300)"
justify="end"
>
<ButtonGroup variant="clear" colorScheme="secondary" spacing={0}>
<IconButton
aria-label="Duplicate field"
icon={<BiDuplicate fontSize="1.25rem" />}
/>
<IconButton
colorScheme="danger"
aria-label="Delete field"
icon={<BiTrash fontSize="1.25rem" />}
/>
</ButtonGroup>
</Flex>
</Collapse>
</Flex>
)
}
Loading