Skip to content

Commit

Permalink
Merge branch 'develop' into chore-791
Browse files Browse the repository at this point in the history
  • Loading branch information
vnugent committed May 8, 2023
2 parents 84d04c3 + 34022b3 commit ad56210
Show file tree
Hide file tree
Showing 25 changed files with 1,093 additions and 200 deletions.
21 changes: 12 additions & 9 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,18 @@ export default function Header (props: HeaderProps): JSX.Element {
</div>
<AppAlert
message={
<div className='flex items-center justify-center flex-wrap'>
We created a special&nbsp;
<Link href='/crag/18c5dd5c-8186-50b6-8a60-ae2948c548d1'>
<a className='link-dotted'>
<strong>Test area</strong>
</a>
</Link>&nbsp;for test driving the new edit feature.
&nbsp;<a className='btn btn-xs font-light' href='https://openbeta.substack.com/p/new-year-new-milestone'>Learn more</a>
</div>
<>
<div className='text-sm'>• May 2023: Photo sharing and tagging is temporarily disabled while we're upgrading our media storage.</div>
<div className='text-sm'>
• January 2023: Use this special&nbsp;
<Link href='/crag/18c5dd5c-8186-50b6-8a60-ae2948c548d1'>
<a className='link-dotted font-semibold'>
Test area
</a>
</Link>&nbsp;for test driving the new edit feature&nbsp;<a className='btn-link font-light text-xs' href='https://openbeta.substack.com/p/new-year-new-milestone'>Learn more</a>
</div>

</>
}
/>
</>
Expand Down
2 changes: 1 addition & 1 deletion src/components/UploadPhotoTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default function UploadPhotoTrigger ({ className = '', onUploaded, childr

return (
<div
className={clx(className, uploading ? 'pointer-events-none' : '')} {...getRootProps()} onClick={(e) => {
className={clx('pointer-events-none cursor-not-allowed', className, uploading ? 'pointer-events-none' : '')} {...getRootProps()} onClick={(e) => {
if (status === 'authenticated' && !uploading) {
openFileDialog()
} else {
Expand Down
29 changes: 29 additions & 0 deletions src/components/basecamp/CreateUpdateModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MobileDialog, DialogContent } from '../ui/MobileDialog'
import { ReactElement } from 'react'

interface CreateUpdateModalProps {
isOpen: boolean
setOpen: (arg0: boolean) => void
contentContainer: ReactElement
}

/**
* Modal for creating and updating objects (areas, organizations, etc) in our datastores.
*/
export default function CreateUpdateModal ({
isOpen,
setOpen,
contentContainer
}: CreateUpdateModalProps): JSX.Element {
return (
<MobileDialog
modal
open={isOpen}
onOpenChange={setOpen}
>
<DialogContent onInteractOutside={() => setOpen(false)}>
{contentContainer}
</DialogContent>
</MobileDialog>
)
}
262 changes: 262 additions & 0 deletions src/components/basecamp/OrganizationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { Input, TextArea, Select } from '../ui/form'
import { useForm, FormProvider } from 'react-hook-form'
import { OrganizationType, OrgType, OrganizationEditableFieldsType, RulesType } from '../../js/types'
import clx from 'classnames'
import { graphqlClient } from '../../js/graphql/Client'
import { useMutation } from '@apollo/client'
import { useSession } from 'next-auth/react'
import {
MUTATION_ADD_ORGANIZATION,
MUTATION_UPDATE_ORGANIZATION,
QUERY_ORGANIZATIONS,
AddOrganizationProps,
UpdateOrganizationProps
} from '../../js/graphql/gql/organization'
import { toast } from 'react-toastify'
import { validate as uuidValidate } from 'uuid'

const DISPLAY_NAME_FORM_VALIDATION_RULES: RulesType = {
required: 'A display name is required.',
minLength: {
value: 2,
message: 'Minimum 2 characters.'
}
}

const MUUID_VALIDATION = {
validate: (value: string) => {
return conjoinedStringToArray(value).every(uuidValidate) || 'Expected comma-separated MUUID hex strings eg. 49017dad-7baf-5fde-8078-f3a4b1230bbb, 88352d11-eb85-5fde-8078-889bb1230b11...'
}
}

interface HtmlFormProps extends OrganizationEditableFieldsType {
conjoinedAssociatedAreaIds: string // Form will return one large conjoined string
conjoinedExcludedAreaIds: string // Form will return one large conjoined string
orgId: string
orgType: OrgType
}

interface OrganizationFormProps {
existingOrg: OrganizationType | null
onClose: () => void
}

/*
* Form for creating and updating organzations.
* When existingOrg is null, form creates new org.
* When not null, form updates the existingOrg instead.
*/
export default function OrganizationForm ({ existingOrg, onClose }: OrganizationFormProps): JSX.Element {
const session = useSession()
const [addOrganization] = useMutation<{ addOrganization: OrganizationType }, { input: AddOrganizationProps }>(
MUTATION_ADD_ORGANIZATION, {
client: graphqlClient,
onError: (error) => toast.error(`Unexpected error: ${error.message}`),
refetchQueries: [{
query: QUERY_ORGANIZATIONS,
variables: {
sort: { updatedAt: -1 },
limit: 20
}
}]
}
)
const [updateOrganization] = useMutation<{ updateOrganization: OrganizationType }, { input: UpdateOrganizationProps }>(
MUTATION_UPDATE_ORGANIZATION, {
client: graphqlClient,
onError: (error) => toast.error(`Unexpected error: ${error.message}`),
refetchQueries: [{
query: QUERY_ORGANIZATIONS,
variables: {
sort: { updatedAt: -1 },
limit: 20
}
}]
}
)
// React-hook-form declaration
const form = useForm<HtmlFormProps>({
mode: 'onBlur',
defaultValues: {
displayName: existingOrg?.displayName ?? '',
orgType: existingOrg?.orgType ?? OrgType.LOCAL_CLIMBING_ORGANIZATION,
conjoinedAssociatedAreaIds: existingOrg?.associatedAreaIds?.join(', ') ?? '',
conjoinedExcludedAreaIds: existingOrg?.excludedAreaIds?.join(', ') ?? '',
description: existingOrg?.content?.description ?? '',
website: existingOrg?.content?.website ?? '',
email: existingOrg?.content?.email ?? '',
instagramLink: existingOrg?.content?.instagramLink ?? '',
donationLink: existingOrg?.content?.donationLink ?? '',
hardwareReportLink: existingOrg?.content?.hardwareReportLink ?? '',
facebookLink: existingOrg?.content?.facebookLink ?? ''
}
})

const { handleSubmit, formState: { isSubmitting, dirtyFields } } = form

/**
* Routes to addOrganization or updateOrganization GraphQL calls
* based on whether there was an existing org to update or not.
* @param formProps Data populated by the user in the form
* @returns
*/
const submitHandler = async ({
orgType,
displayName,
conjoinedAssociatedAreaIds,
conjoinedExcludedAreaIds,
website,
email,
donationLink,
facebookLink,
instagramLink,
hardwareReportLink,
description
}: HtmlFormProps): Promise<void> => {
const dirtyEditableFields: OrganizationEditableFieldsType = {
...dirtyFields?.displayName === true && { displayName },
...dirtyFields?.conjoinedAssociatedAreaIds === true && { associatedAreaIds: conjoinedStringToArray(conjoinedAssociatedAreaIds) },
...dirtyFields?.conjoinedExcludedAreaIds === true && { excludedAreaIds: conjoinedStringToArray(conjoinedExcludedAreaIds) },
...dirtyFields?.website === true && { website },
...dirtyFields?.email === true && { email },
...dirtyFields?.donationLink === true && { donationLink },
...dirtyFields?.facebookLink === true && { facebookLink },
...dirtyFields?.instagramLink === true && { instagramLink },
...dirtyFields?.hardwareReportLink === true && { hardwareReportLink },
...dirtyFields?.description === true && { description }
}
if (existingOrg == null) {
const input: AddOrganizationProps = {
orgType,
...dirtyEditableFields
}
await addOrganization({
variables: { input },
context: {
headers: {
authorization: `Bearer ${session?.data?.accessToken as string ?? ''}`
}
}
})
} else {
const input: UpdateOrganizationProps = {
orgId: existingOrg.orgId,
...dirtyEditableFields
}
await updateOrganization({
variables: { input },
context: {
headers: {
authorization: `Bearer ${session?.data?.accessToken as string ?? ''}`
}
}
})
}
onClose()
}

return (
<div className='px-8 pt-12 pb-4 flex flex-row flex-wrap'>
<div className='basis-1/3 shink-0 grow min-w-[10em] pr-4'>
<h2>Organization Editor</h2>
{existingOrg !== null && ( // Id, OrgId, OrgTypes are immutable and only exist during updating.
<div className='mt-4'>
<div className='mt-4 break-words'>
<h4>orgType</h4>
<p className='text-xs'>{existingOrg.orgType}</p>
</div>
<div className='mt-4'>
<h4>orgId</h4>
<p className='text-xs'>{existingOrg.orgId}</p>
</div>
</div>
)}
</div>
<div className='basis-2/3 grow'>
<FormProvider {...form}>
<form onSubmit={handleSubmit(submitHandler)} className='min-w-[16em]'>
<Input
label='Display Name:'
name='displayName'
placeholder='Climbing Org'
registerOptions={DISPLAY_NAME_FORM_VALIDATION_RULES}
/>
{existingOrg == null &&
<Select
label='Organization Type:'
name='orgType'
options={['LOCAL_CLIMBING_ORGANIZATION']}
defaultOption='LOCAL_CLIMBING_ORGANIZATION'
/>}
<TextArea
label='Description:'
name='description'
placeholder='Seattle-based group founded in 1979 to steward climbing areas across the Pacific Northwest.'
rows={2}
/>
<Input
label='Associated Area Ids:'
name='conjoinedAssociatedAreaIds'
placeholder='49017dad-7baf-5fde-8078-f3a4b1230bbb, 59e17fad-6bb8-de47-aa80-bba4b1a29be1'
registerOptions={MUUID_VALIDATION}
/>
<Input
label='Excluded Area Ids:'
labelAlt='Areas the organization explicitly chooses not to be associated with. Takes precedence over Associated Area Ids.'
name='conjoinedExcludedAreaIds'
placeholder='88352d11-eb85-5fde-8078-889bb1230b11'
registerOptions={MUUID_VALIDATION}
/>
<Input
label='Email:'
name='email'
placeholder='admin@climbingorg.com'
/>
<Input
label='Website:'
name='website'
placeholder='https://www.climbingorg.com'
/>
<Input
label='Instagram:'
name='instagramLink'
placeholder='https://www.instagram.com/climbingorg'
/>
<Input
label='Facebook:'
name='facebookLink'
placeholder='https://www.facebook.com/climbingorg'
/>
<Input
label='Hardware Report Link:'
name='hardwareReportLink'
placeholder='https://www.climbingorg.com/reporthardware'
/>
<Input
label='Donation Link:'
name='donationLink'
placeholder='https://www.climbingorg.com/donate'
/>
<button
className={
clx('mt-4 btn btn-primary w-full',
isSubmitting ? 'loading btn-disabled' : ''
)
}
type='submit'
>Save
</button>
</form>
</FormProvider>
</div>
</div>
)
}

/**
* Convert comma-separated string to array.
* Notably, '' and ',' return [].
*/
function conjoinedStringToArray (conjoined: string): string[] {
return conjoined.split(',').map(s => s.trim()).filter(s => s !== '')
}
Loading

0 comments on commit ad56210

Please sign in to comment.