diff --git a/packages/sanity/src/structure/components/requestPermissionDialog/RequestPermissionDialog.tsx b/packages/sanity/src/structure/components/requestPermissionDialog/RequestPermissionDialog.tsx new file mode 100644 index 00000000000..30931efbc73 --- /dev/null +++ b/packages/sanity/src/structure/components/requestPermissionDialog/RequestPermissionDialog.tsx @@ -0,0 +1,196 @@ +/* eslint-disable i18next/no-literal-string */ + +import {Box, Card, Flex, Stack, Text, TextInput, useToast} from '@sanity/ui' +import {useEffect, useId, useMemo, useState} from 'react' +import {type Role, useClient, useProjectId, useTranslation} from 'sanity' +import {styled} from 'styled-components' + +import {Dialog} from '../../../ui-components' +import {structureLocaleNamespace} from '../../i18n' + +interface AccessRequest { + id: string + status: 'pending' | 'accepted' | 'declined' + resourceId: string + resourceType: 'project' + createdAt: string + updatedAt: string + updatedByUserId: string + requestedByUserId: string + requestedRole: string + requestType: string + note: string +} + +const MAX_NOTE_LENGTH = 150 + +/** @internal */ +export const DialogBody = styled(Box)` + box-sizing: border-box; +` + +/** @internal */ +export const LoadingContainer = styled(Flex).attrs({ + align: 'center', + direction: 'column', + justify: 'center', +})` + height: 110px; +` + +/** @internal */ +export interface RequestPermissionDialogProps { + onCancel?: () => void + onRequstSubmitted?: () => void +} + +/** + * A confirmation dialog used to prevent unwanted document deletes. Loads all + * the referencing internal and cross-data references prior to showing the + * delete button. + * + * @internal + */ +export function RequestPermissionDialog({ + onCancel, + onRequstSubmitted, +}: RequestPermissionDialogProps) { + const {t} = useTranslation(structureLocaleNamespace) + const dialogId = `request-permissions-${useId()}` + const projectId = useProjectId() + const client = useClient() + const toast = useToast() + + const [isSubmitting, setIsSubmitting] = useState(false) + + const [roles, setRoles] = useState() + + const [note, setNote] = useState('') + const [noteLength, setNoteLength] = useState(0) + + const [msgError, setMsgError] = useState() + const [hasTooManyRequests, setHasTooManyRequests] = useState(true) + const [hasBeenDenied, setHasBeenDenied] = useState(false) + + useEffect(() => { + if (!projectId || !client) return + client + .request({ + url: `/projects/${projectId}/roles`, + method: 'get', + }) + .then((data) => setRoles(data)) + }, [projectId, client]) + + const requestedRole = useMemo(() => { + const hasEditor = roles?.find((role) => role.name === 'editor') + return hasEditor ? 'Editor' : 'Administrator' + }, [roles]) + + const onConfirm = () => { + setIsSubmitting(true) + client + .request({ + url: `/access/project/${projectId}/requests`, + method: 'post', + body: {note, requestUrl: window?.location.href, requestedRole, requestType: 'role'}, + }) + .then((request) => { + if (request) { + if (onRequstSubmitted) onRequstSubmitted() + toast.push({title: 'Edit access requested'}) + } + }) + .catch((err) => { + const statusCode = err?.response?.statusCode + const errMessage = err?.response?.body?.message + if (statusCode === 429) { + // User is over their cross-project request limit + setHasTooManyRequests(true) + setMsgError(errMessage) + } + if (statusCode === 409) { + // If we get a 409, user has been denied on this project or has a valid pending request + // valid pending request should be handled by GET request above + setHasBeenDenied(true) + setMsgError(errMessage) + } else { + toast.push({ + title: 'There was a problem submitting your request.', + status: errMessage, + }) + } + }) + .finally(() => { + setIsSubmitting(false) + }) + setIsSubmitting(false) + } + + return ( + + + + + A request will be made to administrators asking to grant you increased permission to + this project. + + If you'd like, you can add a note + {hasTooManyRequests || hasBeenDenied ? ( + + + {hasTooManyRequests && ( + <> + {msgError ?? + `You've reached the limit for role requests across all projects. Please wait + before submitting more requests or contact an admin for assistance.`} + + )} + {hasBeenDenied && ( + <>{msgError ?? `Your request to access this project has been declined.`} + )} + + + ) : ( + + { + if (e.key === 'Enter') onConfirm() + }} + maxLength={MAX_NOTE_LENGTH} + value={note} + onChange={(e) => { + setNote(e.currentTarget.value) + setNoteLength(e.currentTarget.value.length) + }} + /> + + {`${noteLength}/${MAX_NOTE_LENGTH}`} + + )} + + + + ) +}