Merge pull request #2551 from payloadcms/fix/doc-drawer-access

fix: document drawer access control
This commit is contained in:
Jacob Fletcher
2023-04-24 16:56:17 -04:00
committed by GitHub
4 changed files with 165 additions and 59 deletions

View File

@@ -12,20 +12,20 @@ import Button from '../Button';
import { useConfig } from '../../utilities/Config';
import { useLocale } from '../../utilities/Locale';
import { useAuth } from '../../utilities/Auth';
import { DocumentInfoProvider } from '../../utilities/DocumentInfo';
import { DocumentInfoProvider, useDocumentInfo } from '../../utilities/DocumentInfo';
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
import usePayloadAPI from '../../../hooks/usePayloadAPI';
import formatFields from '../../views/collections/Edit/formatFields';
import { useRelatedCollections } from '../../forms/field-types/Relationship/AddNew/useRelatedCollections';
import IDLabel from '../IDLabel';
import { baseClass } from '.';
import { CollectionPermission } from '../../../../auth';
export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
const Content: React.FC<DocumentDrawerProps> = ({
collectionSlug,
id,
drawerSlug,
onSave: onSaveFromProps,
customHeader,
onSave,
}) => {
const { serverURL, routes: { api } } = useConfig();
const { toggleModal, modalState, closeModal } = useModal();
@@ -36,20 +36,23 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
const hasInitializedState = useRef(false);
const [isOpen, setIsOpen] = useState(false);
const [collectionConfig] = useRelatedCollections(collectionSlug);
const { docPermissions, id } = useDocumentInfo();
const [fields, setFields] = useState(() => formatFields(collectionConfig, true));
// no need to an additional requests when creating new documents
const initialID = useRef(id);
const [{ data, isLoading: isLoadingDocument, isError }] = usePayloadAPI(
(initialID.current ? `${serverURL}${api}/${collectionSlug}/${initialID.current}` : null),
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
);
useEffect(() => {
setFields(formatFields(collectionConfig, true));
}, [collectionSlug, collectionConfig]);
const [{ data, isLoading: isLoadingDocument, isError }] = usePayloadAPI(
(id ? `${serverURL}${api}/${collectionSlug}/${id}` : null),
{ initialParams: { 'fallback-locale': 'null', depth: 0, draft: 'true' } },
);
useEffect(() => {
if (isLoadingDocument) {
if (isLoadingDocument || hasInitializedState.current) {
return;
}
@@ -81,62 +84,87 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
}
}, [isError, t, isOpen, data, drawerSlug, closeModal, isLoadingDocument]);
if (isError) return null;
const isEditing = Boolean(id);
const apiURL = id ? `${serverURL}${api}/${collectionSlug}/${id}` : null;
const action = `${serverURL}${api}/${collectionSlug}${id ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`;
const hasSavePermission = (isEditing && docPermissions?.update?.permission) || (!isEditing && (docPermissions as CollectionPermission)?.create?.permission);
const isLoading = !internalState || !docPermissions || isLoadingDocument;
return (
<RenderCustomComponent
DefaultComponent={DefaultEdit}
CustomComponent={collectionConfig.admin?.components?.views?.Edit}
componentProps={{
isLoading,
data,
id,
collection: collectionConfig,
permissions: permissions.collections[collectionConfig.slug],
isEditing,
apiURL,
onSave,
internalState,
hasSavePermission,
action,
disableEyebrow: true,
disableActions: true,
me: true,
disableLeaveWithoutSaving: true,
customHeader: (
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__header-content`}>
<h2 className={`${baseClass}__header-text`}>
{!customHeader ? t(!id ? 'fields:addNewLabel' : 'general:editLabel', { label: getTranslation(collectionConfig.labels.singular, i18n) }) : customHeader}
</h2>
<Button
buttonStyle="none"
className={`${baseClass}__header-close`}
onClick={() => toggleModal(drawerSlug)}
aria-label={t('general:close')}
>
<X />
</Button>
</div>
{id && (
<IDLabel id={id.toString()} />
)}
</div>
),
}}
/>
);
};
// First provide the document context using `DocumentInfoProvider`
// this is so we can utilize the `useDocumentInfo` hook in the `Content` component
// this drawer is used for both creating and editing documents
// this means that the `id` may be unknown until the document is created
export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = (props) => {
const { collectionSlug, id: idFromProps, onSave: onSaveFromProps } = props;
const [collectionConfig] = useRelatedCollections(collectionSlug);
const [id, setId] = useState<string | null>(idFromProps);
const onSave = useCallback<DocumentDrawerProps['onSave']>((args) => {
setId(args.doc.id);
if (typeof onSaveFromProps === 'function') {
onSaveFromProps({
...args,
collectionConfig,
});
}
}, [collectionConfig, onSaveFromProps]);
if (isError) return null;
}, [onSaveFromProps, collectionConfig]);
return (
<DocumentInfoProvider
collection={collectionConfig}
id={id}
>
<RenderCustomComponent
DefaultComponent={DefaultEdit}
CustomComponent={collectionConfig.admin?.components?.views?.Edit}
componentProps={{
isLoading: !internalState,
data,
id,
collection: collectionConfig,
permissions: permissions.collections[collectionConfig.slug],
isEditing: Boolean(id),
apiURL: id ? `${serverURL}${api}/${collectionSlug}/${id}` : null,
onSave,
internalState,
hasSavePermission: true,
action: `${serverURL}${api}/${collectionSlug}${id ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`,
disableEyebrow: true,
disableActions: true,
me: true,
disableLeaveWithoutSaving: true,
customHeader: (
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__header-content`}>
<h2 className={`${baseClass}__header-text`}>
{!customHeader ? t(!id ? 'fields:addNewLabel' : 'general:editLabel', { label: getTranslation(collectionConfig.labels.singular, i18n) }) : customHeader}
</h2>
<Button
buttonStyle="none"
className={`${baseClass}__header-close`}
onClick={() => toggleModal(drawerSlug)}
aria-label={t('general:close')}
>
<X />
</Button>
</div>
{id && (
<IDLabel id={id} />
)}
</div>
),
}}
<Content
{...props}
onSave={onSave}
/>
</DocumentInfoProvider>
);

View File

@@ -92,7 +92,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const json: Permissions = await request.json();
setPermissions(json);
} else {
throw new Error("Fetching permissions failed with status code " + request.status);
throw new Error(`Fetching permissions failed with status code ${request.status}`);
}
}, [serverURL, api, i18n]);
@@ -135,7 +135,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
if (id) {
refreshPermissions();
}
}, [i18n, id, api, serverURL]);
}, [i18n, id, api, serverURL, refreshPermissions]);
useEffect(() => {
let reminder: ReturnType<typeof setTimeout>;