From 373cb0013902b52aba455542e10402316da4b2f4 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:12:19 -0400 Subject: [PATCH] fix: scopes uploadEdits to documents, hoists action to doc provider (#6664) Fixes https://github.com/payloadcms/payload/issues/6545 ### Description Correctly scopes upload edits to a single doc, previously they were stored on the top level document. - Removes formQueryParams in favor of an upload edit provider. - Hoists the document `action` up to the doc provider --- .../elements/DocumentDrawer/DrawerContent.tsx | 12 +--- .../components/elements/EditUpload/index.tsx | 15 ++--- .../utilities/DocumentInfo/index.tsx | 32 ++++++++++- .../utilities/DocumentInfo/types.ts | 1 + .../utilities/FormQueryParams/index.tsx | 21 ------- .../utilities/UploadEdits/index.tsx | 28 ++++++++++ .../admin/components/views/Global/index.tsx | 18 ++++-- .../views/collections/Edit/Default.tsx | 1 - .../views/collections/Edit/index.tsx | 56 +++++-------------- test/globals/config.ts | 12 ++++ test/globals/e2e.spec.ts | 29 ++++++++++ test/uploads/config.ts | 13 +++++ test/uploads/e2e.spec.ts | 17 +++++- test/uploads/shared.ts | 1 + 14 files changed, 163 insertions(+), 93 deletions(-) delete mode 100644 packages/payload/src/admin/components/utilities/FormQueryParams/index.tsx create mode 100644 packages/payload/src/admin/components/utilities/UploadEdits/index.tsx create mode 100644 test/globals/e2e.spec.ts diff --git a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx index d6d1b57f4f..07f08807a8 100644 --- a/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/payload/src/admin/components/elements/DocumentDrawer/DrawerContent.tsx @@ -1,5 +1,4 @@ import { useModal } from '@faceless-ui/modal' -import queryString from 'qs' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'react-toastify' @@ -18,7 +17,6 @@ import X from '../../icons/X' import { useAuth } from '../../utilities/Auth' import { useConfig } from '../../utilities/Config' import { DocumentInfoProvider, useDocumentInfo } from '../../utilities/DocumentInfo' -import { useFormQueryParams } from '../../utilities/FormQueryParams' import { useLocale } from '../../utilities/Locale' import RenderCustomComponent from '../../utilities/RenderCustomComponent' import DefaultEdit from '../../views/collections/Edit/Default' @@ -45,12 +43,10 @@ const Content: React.FC = ({ const [isOpen, setIsOpen] = useState(false) const [collectionConfig] = useRelatedCollections(collectionSlug) const config = useConfig() - const { formQueryParams } = useFormQueryParams() - const formattedQueryParams = queryString.stringify(formQueryParams) const { admin: { components: { views: { Edit } = {} } = {} } = {} } = collectionConfig - const { id, docPermissions, getDocPreferences } = useDocumentInfo() + const { id, action, docPermissions, getDocPreferences } = useDocumentInfo() // If they are replacing the entire edit view, use that. // Else let the DefaultEdit determine what to render. @@ -90,7 +86,7 @@ const Content: React.FC = ({ setInternalState(state) } - awaitInitialState() + void awaitInitialState() hasInitializedState.current = true }, [data, fields, id, user, locale, isLoadingDocument, t, getDocPreferences, config]) @@ -111,10 +107,6 @@ const Content: React.FC = ({ const apiURL = id ? `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale}` : null - const action = `${serverURL}${api}/${collectionSlug}${ - isEditing ? `/${id}` : '' - }?${formattedQueryParams}` - const hasSavePermission = (isEditing && docPermissions?.update?.permission) || (!isEditing && (docPermissions as CollectionPermission)?.create?.permission) diff --git a/packages/payload/src/admin/components/elements/EditUpload/index.tsx b/packages/payload/src/admin/components/elements/EditUpload/index.tsx index 4d9c29f560..e8fee73e03 100644 --- a/packages/payload/src/admin/components/elements/EditUpload/index.tsx +++ b/packages/payload/src/admin/components/elements/EditUpload/index.tsx @@ -7,7 +7,7 @@ import 'react-image-crop/dist/ReactCrop.css' import type { Data } from '../../forms/Form/types' import Plus from '../../icons/Plus' -import { useFormQueryParams } from '../../utilities/FormQueryParams' +import { useUploadEdits } from '../../utilities/UploadEdits' import { editDrawerSlug } from '../../views/collections/Edit/Upload' import Button from '../Button' import './index.scss' @@ -35,8 +35,8 @@ export const EditUpload: React.FC<{ }> = ({ doc, fileName, fileSrc, imageCacheTag, showCrop, showFocalPoint }) => { const { closeModal } = useModal() const { t } = useTranslation(['general', 'upload']) - const { formQueryParams, setFormQueryParams } = useFormQueryParams() - const { uploadEdits } = formQueryParams || {} + const { updateUploadEdits, uploadEdits } = useUploadEdits() + const [crop, setCrop] = useState({ height: uploadEdits?.crop?.height || 100, unit: '%', @@ -87,12 +87,9 @@ export const EditUpload: React.FC<{ } const saveEdits = () => { - setFormQueryParams({ - ...formQueryParams, - uploadEdits: { - crop: crop || undefined, - focalPoint: focalPosition ? focalPosition : undefined, - }, + updateUploadEdits({ + crop: crop || undefined, + focalPoint: focalPosition ? focalPosition : undefined, }) closeModal(editDrawerSlug) } diff --git a/packages/payload/src/admin/components/utilities/DocumentInfo/index.tsx b/packages/payload/src/admin/components/utilities/DocumentInfo/index.tsx index 62740af7ac..3bb725cc2d 100644 --- a/packages/payload/src/admin/components/utilities/DocumentInfo/index.tsx +++ b/packages/payload/src/admin/components/utilities/DocumentInfo/index.tsx @@ -1,4 +1,5 @@ import qs from 'qs' +import QueryString from 'qs' import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useParams } from 'react-router-dom' @@ -14,12 +15,13 @@ import { useAuth } from '../Auth' import { useConfig } from '../Config' import { useLocale } from '../Locale' import { usePreferences } from '../Preferences' +import { UploadEditsProvider, useUploadEdits } from '../UploadEdits' const Context = createContext({} as ContextType) export const useDocumentInfo = (): ContextType => useContext(Context) -export const DocumentInfoProvider: React.FC = ({ +const DocumentInfo: React.FC = ({ id: idFromProps, children, collection, @@ -37,6 +39,7 @@ export const DocumentInfoProvider: React.FC = ({ const { i18n } = useTranslation() const { permissions } = useAuth() const { code } = useLocale() + const { uploadEdits } = useUploadEdits() const [publishedDoc, setPublishedDoc] = useState(null) const [versions, setVersions] = useState>(null) const [unpublishedVersions, setUnpublishedVersions] = useState>(null) @@ -256,16 +259,31 @@ export const DocumentInfoProvider: React.FC = ({ ) useEffect(() => { - getVersions() + void getVersions() }, [getVersions]) useEffect(() => { - getDocPermissions() + void getDocPermissions() }, [getDocPermissions]) + const action: string = React.useMemo(() => { + const docURL = `${baseURL}${pluralType === 'globals' ? `/globals` : ''}/${slug}${id ? `/${id}` : ''}` + const params = { + depth: 0, + 'fallback-locale': 'null', + locale: code, + uploadEdits: uploadEdits || undefined, + } + + return `${docURL}${QueryString.stringify(params, { + addQueryPrefix: true, + })}` + }, [baseURL, code, pluralType, id, slug, uploadEdits]) + const value: ContextType = { id, slug, + action, collection, docPermissions, getDocPermissions, @@ -281,3 +299,11 @@ export const DocumentInfoProvider: React.FC = ({ return {children} } + +export const DocumentInfoProvider: React.FC = (props) => { + return ( + + + + ) +} diff --git a/packages/payload/src/admin/components/utilities/DocumentInfo/types.ts b/packages/payload/src/admin/components/utilities/DocumentInfo/types.ts index 97e07183ea..af6cec0323 100644 --- a/packages/payload/src/admin/components/utilities/DocumentInfo/types.ts +++ b/packages/payload/src/admin/components/utilities/DocumentInfo/types.ts @@ -15,6 +15,7 @@ export type Version = TypeWithVersion export type DocumentPermissions = CollectionPermission | GlobalPermission export type ContextType = { + action: string collection?: SanitizedCollectionConfig docPermissions: DocumentPermissions getDocPermissions: () => Promise diff --git a/packages/payload/src/admin/components/utilities/FormQueryParams/index.tsx b/packages/payload/src/admin/components/utilities/FormQueryParams/index.tsx deleted file mode 100644 index 8cfd349c8f..0000000000 --- a/packages/payload/src/admin/components/utilities/FormQueryParams/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { createContext, useContext } from 'react' - -import type { UploadEdits } from '../../views/collections/Edit/types' - -export type QueryParamTypes = { - depth: number - 'fallback-locale': string - locale: string - uploadEdits?: UploadEdits -} -export const FormQueryParams = createContext( - {} as { - formQueryParams: QueryParamTypes - setFormQueryParams: (params: QueryParamTypes) => void - }, -) - -export const useFormQueryParams = (): { - formQueryParams: QueryParamTypes - setFormQueryParams: (params: QueryParamTypes) => void -} => useContext(FormQueryParams) diff --git a/packages/payload/src/admin/components/utilities/UploadEdits/index.tsx b/packages/payload/src/admin/components/utilities/UploadEdits/index.tsx new file mode 100644 index 0000000000..442264d6b8 --- /dev/null +++ b/packages/payload/src/admin/components/utilities/UploadEdits/index.tsx @@ -0,0 +1,28 @@ +import React from 'react' + +import type { UploadEdits } from '../../../../uploads/types' + +export type UploadEditsContext = { + updateUploadEdits: (edits: UploadEdits) => void + uploadEdits: UploadEdits +} + +const Context = React.createContext({ + updateUploadEdits: undefined, + uploadEdits: undefined, +}) + +export const UploadEditsProvider = ({ children }) => { + const [uploadEdits, setUploadEdits] = React.useState(undefined) + + const updateUploadEdits = (edits: UploadEdits) => { + setUploadEdits((prevEdits) => ({ + ...(prevEdits || {}), + ...(edits || {}), + })) + } + + return {children} +} + +export const useUploadEdits = (): UploadEditsContext => React.useContext(Context) diff --git a/packages/payload/src/admin/components/views/Global/index.tsx b/packages/payload/src/admin/components/views/Global/index.tsx index b34e5ec6aa..5c465e39fa 100644 --- a/packages/payload/src/admin/components/views/Global/index.tsx +++ b/packages/payload/src/admin/components/views/Global/index.tsx @@ -27,8 +27,14 @@ const GlobalView: React.FC = (props) => { const { permissions, user } = useAuth() const [initialState, setInitialState] = useState() const [updatedAt, setUpdatedAt] = useState() - const { docPermissions, getDocPermissions, getDocPreferences, getVersions, preferencesKey } = - useDocumentInfo() + const { + action, + docPermissions, + getDocPermissions, + getDocPreferences, + getVersions, + preferencesKey, + } = useDocumentInfo() const { getPreference } = usePreferences() const { t } = useTranslation() const config = useConfig() @@ -49,8 +55,8 @@ const GlobalView: React.FC = (props) => { updatedAt: json?.result?.updatedAt || new Date().toISOString(), }) - getVersions() - getDocPermissions() + void getVersions() + void getDocPermissions() setUpdatedAt(json?.result?.updatedAt) const preferences = await getDocPreferences() @@ -109,7 +115,7 @@ const GlobalView: React.FC = (props) => { setInitialState(state) } - if (dataToRender) awaitInitialState() + if (dataToRender) void awaitInitialState() }, [ dataToRender, fields, @@ -125,7 +131,7 @@ const GlobalView: React.FC = (props) => { const isLoading = !initialState || !docPermissions || isLoadingData const componentProps: DefaultGlobalViewProps = { - action: `${serverURL}${api}/globals/${slug}?locale=${locale}&fallback-locale=null`, + action, apiURL: `${serverURL}${api}/globals/${slug}?locale=${locale}${ global.versions?.drafts ? '&draft=true' : '' }`, diff --git a/packages/payload/src/admin/components/views/collections/Edit/Default.tsx b/packages/payload/src/admin/components/views/collections/Edit/Default.tsx index 2246f78842..21fde36155 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/Default.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/Default.tsx @@ -46,7 +46,6 @@ const DefaultEditView: React.FC = (props) => { } = props const { setViewActions } = useActions() - const { reportUpdate } = useDocumentEvents() const { auth } = collection diff --git a/packages/payload/src/admin/components/views/collections/Edit/index.tsx b/packages/payload/src/admin/components/views/collections/Edit/index.tsx index 432a7db7a1..a496d0d509 100644 --- a/packages/payload/src/admin/components/views/collections/Edit/index.tsx +++ b/packages/payload/src/admin/components/views/collections/Edit/index.tsx @@ -1,11 +1,9 @@ -import queryString from 'qs' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useHistory, useRouteMatch } from 'react-router-dom' import type { CollectionPermission } from '../../../../../auth' import type { Fields } from '../../../forms/Form/types' -import type { QueryParamTypes } from '../../../utilities/FormQueryParams' import type { DefaultEditViewProps } from './Default' import type { IndexProps } from './types' @@ -16,7 +14,6 @@ import { useAuth } from '../../../utilities/Auth' import { useConfig } from '../../../utilities/Config' import { useDocumentInfo } from '../../../utilities/DocumentInfo' import { EditDepthContext } from '../../../utilities/EditDepth' -import { FormQueryParams } from '../../../utilities/FormQueryParams' import { useLocale } from '../../../utilities/Locale' import RenderCustomComponent from '../../../utilities/RenderCustomComponent' import NotFound from '../../NotFound' @@ -32,15 +29,6 @@ const EditView: React.FC = (props) => { const [fields] = useState(() => formatFields(incomingCollection, isEditing)) const [collection] = useState(() => ({ ...incomingCollection, fields })) const [redirect, setRedirect] = useState() - const [formQueryParams, setFormQueryParams] = useState({ - depth: 0, - 'fallback-locale': 'null', - locale: '', - uploadEdits: undefined, - }) - - const formattedQueryParams = queryString.stringify(formQueryParams) - const { code: locale } = useLocale() const config = useConfig() @@ -56,7 +44,8 @@ const EditView: React.FC = (props) => { const [updatedAt, setUpdatedAt] = useState() const { permissions, user } = useAuth() const userRef = useRef(user) - const { docPermissions, getDocPermissions, getDocPreferences, getVersions } = useDocumentInfo() + const { action, docPermissions, getDocPermissions, getDocPreferences, getVersions } = + useDocumentInfo() const { t } = useTranslation('general') const [{ data, isError, isLoading: isLoadingData }, { refetchData }] = usePayloadAPI( @@ -87,20 +76,16 @@ const EditView: React.FC = (props) => { ) const onSave = useCallback( - async (json: { doc }) => { - getVersions() - getDocPermissions() + (json: { doc }) => { + void getVersions() + void getDocPermissions() setUpdatedAt(json?.doc?.updatedAt) if (!isEditing) { setRedirect(`${admin}/collections/${collection.slug}/${json?.doc?.id}`) } else { - buildState(json.doc, { + void buildState(json.doc, { fieldSchema: collection.fields, }) - setFormQueryParams((params) => ({ - ...params, - uploadEdits: undefined, - })) } }, [admin, getVersions, isEditing, buildState, getDocPermissions, collection], @@ -108,15 +93,15 @@ const EditView: React.FC = (props) => { useEffect(() => { if (fields && (isEditing ? data : true)) { - const awaitInternalState = async () => { + const awaitInternalState = () => { setUpdatedAt(data?.updatedAt) - buildState(data, { + void buildState(data, { fieldSchema: fields, operation: isEditing ? 'update' : 'create', }) } - awaitInternalState() + void awaitInternalState() } }, [isEditing, data, buildState, fields]) @@ -126,13 +111,6 @@ const EditView: React.FC = (props) => { } }, [history, redirect]) - useEffect(() => { - setFormQueryParams((params) => ({ - ...params, - locale, - })) - }, [locale]) - useEffect(() => { if (history.location.state?.refetchDocumentData) { void refetchData() @@ -147,10 +125,6 @@ const EditView: React.FC = (props) => { collection.versions.drafts ? '&draft=true' : '' }` - const action = `${serverURL}${api}/${collectionSlug}${ - isEditing ? `/${id}` : '' - }?${formattedQueryParams}` - const hasSavePermission = (isEditing && docPermissions?.update?.permission) || (!isEditing && (docPermissions as CollectionPermission)?.create?.permission) @@ -177,13 +151,11 @@ const EditView: React.FC = (props) => { return ( - - - + ) } diff --git a/test/globals/config.ts b/test/globals/config.ts index 443e4b2832..d2c2d8379f 100644 --- a/test/globals/config.ts +++ b/test/globals/config.ts @@ -19,6 +19,13 @@ const access = { } export default buildConfigWithDefaults({ + collections: [ + { + slug: 'media', + upload: true, + fields: [], + }, + ], globals: [ { access, @@ -31,6 +38,11 @@ export default buildConfigWithDefaults({ name: 'title', type: 'text', }, + { + name: 'media', + type: 'upload', + relationTo: 'media', + }, ], slug, }, diff --git a/test/globals/e2e.spec.ts b/test/globals/e2e.spec.ts new file mode 100644 index 0000000000..fb84e91840 --- /dev/null +++ b/test/globals/e2e.spec.ts @@ -0,0 +1,29 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' + +import { initPageConsoleErrorCatch } from '../helpers' +import { AdminUrlUtil } from '../helpers/adminUrlUtil' +import { initPayloadE2E } from '../helpers/configHelpers' + +const { beforeAll, describe } = test + +describe('Globals', () => { + let page: Page + let url: AdminUrlUtil + + beforeAll(async ({ browser }) => { + const { serverURL } = await initPayloadE2E(__dirname) + url = new AdminUrlUtil(serverURL, 'media') + + const context = await browser.newContext() + page = await context.newPage() + initPageConsoleErrorCatch(page) + }) + + test('can edit media from field', async () => { + await page.goto(url.create) + + // const textCell = page.locator('.row-1 .cell-text') + }) +}) diff --git a/test/uploads/config.ts b/test/uploads/config.ts index a0a34886d8..d5ef468533 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -12,6 +12,7 @@ import { cropOnlySlug, enlargeSlug, focalOnlySlug, + globalWithMedia, mediaSlug, reduceSlug, relationSlug, @@ -491,6 +492,18 @@ export default buildConfigWithDefaults({ }, }, ], + globals: [ + { + slug: globalWithMedia, + fields: [ + { + type: 'upload', + name: 'media', + relationTo: cropOnlySlug, + }, + ], + }, + ], onInit: async (payload) => { const uploadsDir = path.resolve(__dirname, './media') removeFiles(path.normalize(uploadsDir)) diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index 56cf74bbf1..60ce39ffb7 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -12,7 +12,7 @@ import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' import { RESTClient } from '../helpers/rest' import { adminThumbnailSrc } from './collections/admin-thumbnail' -import { adminThumbnailSlug, audioSlug, mediaSlug, relationSlug } from './shared' +import { adminThumbnailSlug, audioSlug, globalWithMedia, mediaSlug, relationSlug } from './shared' const { beforeAll, describe } = test @@ -21,6 +21,7 @@ let mediaURL: AdminUrlUtil let audioURL: AdminUrlUtil let relationURL: AdminUrlUtil let adminThumbnailURL: AdminUrlUtil +let globalURL: string describe('uploads', () => { let page: Page @@ -36,6 +37,7 @@ describe('uploads', () => { audioURL = new AdminUrlUtil(serverURL, audioSlug) relationURL = new AdminUrlUtil(serverURL, relationSlug) adminThumbnailURL = new AdminUrlUtil(serverURL, adminThumbnailSlug) + globalURL = new AdminUrlUtil(serverURL, globalWithMedia).global(globalWithMedia) const context = await browser.newContext() page = await context.newPage() @@ -323,4 +325,17 @@ describe('uploads', () => { expect(redDoc.filesize).toEqual(1207) }) }) + + describe('globals', () => { + test('should be able to crop media from a global', async () => { + await page.goto(globalURL) + await page.click('.upload__toggler.doc-drawer__toggler') + await page.setInputFiles('input[type="file"]', path.resolve(__dirname, './image.png')) + await page.click('.file-field__edit') + await page.click('.btn.edit-upload__save') + await saveDocAndAssert(page, '.drawer__content #action-save') + await saveDocAndAssert(page) + await expect(page.locator('.thumbnail img')).toBeVisible() + }) + }) }) diff --git a/test/uploads/shared.ts b/test/uploads/shared.ts index 02f503ebe9..96aef50ceb 100644 --- a/test/uploads/shared.ts +++ b/test/uploads/shared.ts @@ -7,3 +7,4 @@ export const mediaSlug = 'media' export const reduceSlug = 'reduce' export const relationSlug = 'relation' export const versionSlug = 'versions' +export const globalWithMedia = 'global-with-media'