-
Notifications
You must be signed in to change notification settings - Fork 441
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add stub of request permission dialog
- Loading branch information
Showing
1 changed file
with
196 additions
and
0 deletions.
There are no files selected for viewing
196 changes: 196 additions & 0 deletions
196
packages/sanity/src/structure/components/requestPermissionDialog/RequestPermissionDialog.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Role[]>() | ||
|
||
const [note, setNote] = useState('') | ||
const [noteLength, setNoteLength] = useState<number>(0) | ||
|
||
const [msgError, setMsgError] = useState<string | undefined>() | ||
const [hasTooManyRequests, setHasTooManyRequests] = useState<boolean>(true) | ||
const [hasBeenDenied, setHasBeenDenied] = useState<boolean>(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<AccessRequest | null>({ | ||
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 ( | ||
<Dialog | ||
width={1} | ||
id={dialogId} | ||
header={'Ask to edit'} | ||
footer={{ | ||
cancelButton: { | ||
onClick: onCancel, | ||
text: t('confirm-delete-dialog.cancel-button.text'), | ||
}, | ||
confirmButton: { | ||
loading: isSubmitting, | ||
disabled: hasTooManyRequests || hasBeenDenied, | ||
text: 'Send request', | ||
tone: 'primary', | ||
onClick: onConfirm, | ||
}, | ||
}} | ||
onClose={onCancel} | ||
onClickOutside={onCancel} | ||
> | ||
<DialogBody> | ||
<Stack space={4}> | ||
<Text> | ||
A request will be made to administrators asking to grant you increased permission to | ||
this project. | ||
</Text> | ||
<Text>If you'd like, you can add a note</Text> | ||
{hasTooManyRequests || hasBeenDenied ? ( | ||
<Card tone={'caution'} padding={3} radius={2} shadow={1}> | ||
<Text size={1}> | ||
{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.`}</> | ||
)} | ||
</Text> | ||
</Card> | ||
) : ( | ||
<Stack space={3} paddingBottom={0}> | ||
<TextInput | ||
placeholder="Add note..." | ||
disabled={isSubmitting} | ||
onKeyDown={(e) => { | ||
if (e.key === 'Enter') onConfirm() | ||
}} | ||
maxLength={MAX_NOTE_LENGTH} | ||
value={note} | ||
onChange={(e) => { | ||
setNote(e.currentTarget.value) | ||
setNoteLength(e.currentTarget.value.length) | ||
}} | ||
/> | ||
|
||
<Text align="right" muted size={1}>{`${noteLength}/${MAX_NOTE_LENGTH}`}</Text> | ||
</Stack> | ||
)} | ||
</Stack> | ||
</DialogBody> | ||
</Dialog> | ||
) | ||
} |