Commit 1d8467b4 authored by Stefan Probst's avatar Stefan Probst
Browse files

feat: add create actor dialog

parent 0de01416
Pipeline #178000 passed with stage
in 5 minutes and 38 seconds
import { Dialog } from '@reach/dialog'
import { useState } from 'react'
import { useGetActors, useGetAllActorRoles } from '@/api/sshoc'
import { useQueryClient } from 'react-query'
import { useCreateActor, useGetActors, useGetAllActorRoles } from '@/api/sshoc'
import type { ActorCore } from '@/api/sshoc'
import { Button } from '@/elements/Button/Button'
import { Icon } from '@/elements/Icon/Icon'
import { Svg as CloseIcon } from '@/elements/icons/small/cross.svg'
import { useToast } from '@/elements/Toast/useToast'
import { useDebouncedState } from '@/lib/hooks/useDebouncedState'
import { useAuth } from '@/modules/auth/AuthContext'
import { FormComboBox } from '@/modules/form/components/FormComboBox/FormComboBox'
import { FormFieldAddButton } from '@/modules/form/components/FormFieldAddButton/FormFieldAddButton'
import { FormFieldRemoveButton } from '@/modules/form/components/FormFieldRemoveButton/FormFieldRemoveButton'
......@@ -9,7 +17,10 @@ import { FormRecord } from '@/modules/form/components/FormRecord/FormRecord'
import { FormRecords } from '@/modules/form/components/FormRecords/FormRecords'
import { FormSection } from '@/modules/form/components/FormSection/FormSection'
import { FormSelect } from '@/modules/form/components/FormSelect/FormSelect'
import { FormTextField } from '@/modules/form/components/FormTextField/FormTextField'
import { Form } from '@/modules/form/Form'
import { FormFieldArray } from '@/modules/form/FormFieldArray'
import { isEmail, isUrl } from '@/modules/form/validate'
export interface ActorsFormSectionProps {
initialValues?: any
......@@ -19,8 +30,23 @@ export interface ActorsFormSectionProps {
* Form section for contributors.
*/
export function ActorsFormSection(props: ActorsFormSectionProps): JSX.Element {
const [showCreateNewDialog, setShowCreateNewDialog] = useState(false)
function openCreateNewDialog() {
setShowCreateNewDialog(true)
}
function closeCreateNewDialog() {
setShowCreateNewDialog(false)
}
return (
<FormSection title={'Actors'}>
<FormSection
title={'Actors'}
actions={
<FormFieldAddButton onPress={openCreateNewDialog}>
{'Create new actor'}
</FormFieldAddButton>
}
>
<FormFieldArray name="contributors">
{({ fields }) => {
return (
......@@ -56,6 +82,10 @@ export function ActorsFormSection(props: ActorsFormSectionProps): JSX.Element {
)
}}
</FormFieldArray>
<CreateActorDialog
isOpen={showCreateNewDialog}
onDismiss={closeCreateNewDialog}
/>
</FormSection>
)
}
......@@ -130,3 +160,171 @@ function ActorComboBox(props: ActorComboBoxProps): JSX.Element {
</FormComboBox>
)
}
interface CreateActorDialogProps {
isOpen: boolean
onDismiss: () => void
}
/**
* Create new actor dialog.
*/
function CreateActorDialog(props: CreateActorDialogProps) {
return (
<Dialog
isOpen={props.isOpen}
onDismiss={props.onDismiss}
className="flex flex-col w-full max-w-screen-lg px-32 py-16 mx-auto bg-white rounded shadow-lg"
style={{ width: '60vw', marginTop: '10vh', marginBottom: '10vh' }}
aria-label="Create new actor"
>
<button
onClick={props.onDismiss}
className="self-end"
aria-label="Close dialog"
>
<Icon icon={CloseIcon} className="" />
</button>
<section className="flex flex-col space-y-6">
<h2 className="text-2xl font-medium">Create new actor</h2>
{/* this form is rendered in a portal, so it's valid html, even though it's a <form> "nested" in another <form>. */}
<CreateActorForm onDismiss={props.onDismiss} />
</section>
</Dialog>
)
}
type ActorFormValues = ActorCore
interface CreateActorFormProps {
onDismiss: () => void
}
/**
* Create actor.
*/
function CreateActorForm(props: CreateActorFormProps) {
const createActor = useCreateActor()
const auth = useAuth()
const queryClient = useQueryClient()
const toast = useToast()
function onSubmit(values: ActorFormValues) {
if (auth.session?.accessToken === undefined) {
toast.error('Authentication required.')
return Promise.reject()
}
return createActor.mutateAsync(
[values, { token: auth.session?.accessToken }],
{
onSuccess() {
queryClient.invalidateQueries(['getActors'])
toast.success('Sucessfully created actor.')
},
onError() {
toast.error('Failed to create actor.')
},
onSettled() {
props.onDismiss()
},
},
)
}
function onValidate(values: Partial<ActorFormValues>) {
const errors: Partial<Record<keyof typeof values, string>> = {}
if (values.name === undefined) {
errors.name = 'Name is required.'
}
if (values.email !== undefined && !isEmail(values.email)) {
errors.email = 'Please provide a valid email address.'
}
if (values.website !== undefined && !isUrl(values.website)) {
errors.website = 'Please provide a valid URL.'
}
return errors
}
return (
<Form onSubmit={onSubmit} validate={onValidate}>
{({ handleSubmit, pristine, invalid, submitting }) => {
return (
<form
noValidate
className="flex flex-col space-y-6"
onSubmit={handleSubmit}
>
<FormTextField
name="name"
label="Name"
isRequired
variant="form"
style={{ flex: 1 }}
/>
<FormTextField
name="email"
label="Email"
variant="form"
style={{ flex: 1 }}
/>
<FormTextField
name="website"
label="Website"
variant="form"
style={{ flex: 1 }}
/>
<FormFieldArray name="affiliations">
{({ fields }) => {
return (
<FormRecords>
{fields.map((name, index) => {
return (
<FormRecord
key={name}
actions={
<FormFieldRemoveButton
onPress={() => fields.remove(index)}
aria-label={'Remove affiliation'}
/>
}
>
<ActorComboBox
name={`${name}.code`}
label="Affiliation"
index={index}
/>
</FormRecord>
)
})}
<FormFieldAddButton onPress={() => fields.push(undefined)}>
{'Add affiliation'}
</FormFieldAddButton>
</FormRecords>
)
}}
</FormFieldArray>
<div className="flex justify-end space-x-12">
<Button variant="link" onPress={props.onDismiss}>
Cancel
</Button>
<Button
type="submit"
variant="gradient"
isDisabled={
pristine || invalid || submitting || createActor.isLoading
}
>
Create
</Button>
</div>
</form>
)
}}
</Form>
)
}
......@@ -22,7 +22,7 @@ export function FormFieldAddButton(
const styles = {
button:
'transition cursor-default self-start inline-flex space-x-1.5 items-center font-body font-normal font-ui-base text-primary-750 hover:text-secondary-600 focus:text-gray-800 focus:outline-none',
'transition cursor-default inline-flex space-x-1.5 items-center font-body font-normal font-ui-base text-primary-750 hover:text-secondary-600 focus:text-gray-800 focus:outline-none',
icon: 'w-2.5 h-2.5',
}
......
import type { ReactNode } from 'react'
import type { CSSProperties, ReactNode } from 'react'
export interface FormSectionProps {
title?: string
/** @default 2 */
headingLevel?: 1 | 2 | 3 | 4
children?: ReactNode
actions?: ReactNode
style?: CSSProperties
}
export function FormSection(props: FormSectionProps): JSX.Element {
......@@ -17,16 +19,21 @@ export function FormSection(props: FormSectionProps): JSX.Element {
}
if (title === undefined) {
return <section className={styles.section}>{props.children}</section>
return (
<section className={styles.section} style={props.style}>
{props.children}
</section>
)
}
return (
<section className={styles.section}>
<section className={styles.section} style={props.style}>
<div className="flex items-baseline space-x-4">
<ElementType className="font-medium text-gray-800 font-body text-ui-3xl">
{title}
</ElementType>
<span className="flex-1 border-b border-gray-200" />
<div className="leading-none">{props.actions}</div>
</div>
{props.children}
</section>
......
......@@ -22,7 +22,7 @@ import '@/styles/nprogress.css'
/** should use ReactToastify.minimal.css */
import 'react-toastify/dist/ReactToastify.css'
import '@/styles/combobox.css'
import '@reach/dialog/styles.css'
import '@/styles/dialog.css'
/**
* Report web vitals.
......
:root {
--reach-dialog: 1;
}
[data-reach-dialog-overlay] {
background: hsla(0, 0%, 0%, 0.33);
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
}
......@@ -9,6 +9,9 @@ module.exports = {
purge: ['src/**/*.tsx', 'content/**/*.mdx'],
theme: {
extend: {
backgroundSize: {
double: '150% 150%',
},
colors: {
gray: {
50: '#FAFAFA',
......@@ -130,7 +133,9 @@ module.exports = {
},
},
variants: {
// boxShadow: ['responsive', 'hover', 'focus', 'focus-visible']
extend: {
backgroundPosition: ['hover', 'focus'],
},
},
plugins: [
require('@tailwindcss/typography')({
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment