diff --git a/.vscode/launch.json b/.vscode/launch.json index fc04075945..8fc202f89d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,13 +3,7 @@ // Hover to view descriptions of existing attributes. "configurations": [ { - "command": "pnpm dev", - "name": "Run Dev 3.0", - "request": "launch", - "type": "node-terminal" - }, - { - "command": "pnpm run dev _community", + "command": "pnpm run dev _community -- --no-turbo", "cwd": "${workspaceFolder}", "name": "Run Dev Community", "request": "launch", diff --git a/packages/next/package.json b/packages/next/package.json index 74e0b4f06a..f517d62e5e 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -78,6 +78,7 @@ "graphql-http": "^1.22.0", "graphql-playground-html": "1.6.30", "path-to-regexp": "^6.2.1", + "qs": "6.11.2", "react-diff-viewer-continued": "3.2.6", "react-toastify": "8.2.0", "sass": "^1.71.1", diff --git a/packages/next/src/utilities/createPayloadRequest.ts b/packages/next/src/utilities/createPayloadRequest.ts index ce4cc10c0a..be1b888956 100644 --- a/packages/next/src/utilities/createPayloadRequest.ts +++ b/packages/next/src/utilities/createPayloadRequest.ts @@ -10,6 +10,7 @@ import { translations } from '@payloadcms/translations/api' import { getAuthenticatedUser } from 'payload/auth' import { parseCookies } from 'payload/auth' import { getDataLoader } from 'payload/utilities' +import QueryString from 'qs' import { URL } from 'url' import { getDataAndFile } from './getDataAndFile' @@ -95,6 +96,7 @@ export const createPayloadRequest = async ({ payloadUploadSizes: {}, port: urlProperties.port, protocol: urlProperties.protocol, + query: urlProperties.search ? QueryString.parse(urlProperties.search) : {}, routeParams: params || {}, search: urlProperties.search, searchParams: urlProperties.searchParams, diff --git a/packages/next/src/views/Account/index.tsx b/packages/next/src/views/Account/index.tsx index 3dd6c62080..bd982b8d64 100644 --- a/packages/next/src/views/Account/index.tsx +++ b/packages/next/src/views/Account/index.tsx @@ -19,7 +19,6 @@ export { generateAccountMetadata } from './meta' export const Account: React.FC = async ({ initPageResult, searchParams }) => { const { - locale, permissions, req: { payload, @@ -94,8 +93,8 @@ export const Account: React.FC = async ({ initPageResult, search } - action={`${serverURL}${api}/${userSlug}/${data?.id}?locale=${locale.code}`} - apiURL={`${serverURL}${api}/${userSlug}/${data?.id}?locale=${locale.code}`} + action={`${serverURL}${api}/${userSlug}${data?.id ? `/${data.id}` : ''}`} + apiURL={`${serverURL}${api}/${userSlug}${data?.id ? `/${data.id}` : ''}`} collectionSlug={userSlug} docPermissions={collectionPermissions} docPreferences={docPreferences} diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index d7d4335a37..8a20628fda 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -170,13 +170,6 @@ export const Document: React.FC = async ({ req, }) - const formQueryParams: QueryParamTypes = { - depth: 0, - 'fallback-locale': 'null', - locale: locale.code, - uploadEdits: undefined, - } - const serverSideProps: ServerSideEditViewProps = { initPageResult, routeSegments: segments, @@ -193,7 +186,7 @@ export const Document: React.FC = async ({ /> = async ({ initialState={initialState} /> - + { } = useConfig() const router = useRouter() + const { dispatchFormQueryParams } = useFormQueryParams() const { getComponentMap } = useComponentMap() @@ -38,16 +40,23 @@ export const EditViewClient: React.FC = () => { if (!isEditing) { router.push(`${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}`) } else { - // buildState(json.doc, { - // fieldSchema: collection.fields, - // }) - // setFormQueryParams((params) => ({ - // ...params, - // uploadEdits: undefined, - // })) + dispatchFormQueryParams({ + type: 'SET', + params: { + uploadEdits: null, + }, + }) } }, - [getVersions, isEditing, getDocPermissions, collectionSlug, adminRoute, router], + [ + adminRoute, + collectionSlug, + dispatchFormQueryParams, + getDocPermissions, + getVersions, + isEditing, + router, + ], ) useEffect(() => { diff --git a/packages/payload/src/types/index.ts b/packages/payload/src/types/index.ts index ec00f9cb32..9af1122527 100644 --- a/packages/payload/src/types/index.ts +++ b/packages/payload/src/types/index.ts @@ -1,6 +1,8 @@ import type { I18n, TFunction } from '@payloadcms/translations' import type DataLoader from 'dataloader' +import { QuerySelector } from 'mongoose' + import type payload from '../' import type { User } from '../auth/types' import type { TypeWithID, TypeWithTimestamps } from '../collections/config/types' @@ -54,6 +56,8 @@ export type CustomPayloadRequest = { payloadDataLoader?: DataLoader /** Resized versions of the image that was uploaded during this request */ payloadUploadSizes?: Record + /** Query params on the request */ + query: Record /** The route parameters * @example * /:collection/:id -> /posts/123 diff --git a/packages/payload/src/uploads/generateFileData.ts b/packages/payload/src/uploads/generateFileData.ts index ae0c4b6859..39f49959fe 100644 --- a/packages/payload/src/uploads/generateFileData.ts +++ b/packages/payload/src/uploads/generateFileData.ts @@ -52,8 +52,7 @@ export const generateFileData = async ({ let file = req.file - const { searchParams } = req - const uploadEdits = searchParams.get('uploadEdits') || {} + const uploadEdits = req.query['uploadEdits'] || {} const { disableLocalStorage, formatOptions, imageSizes, resizeOptions, staticDir, trimOptions } = collectionConfig.upload diff --git a/packages/payload/src/utilities/createLocalReq.ts b/packages/payload/src/utilities/createLocalReq.ts index 7637e56ac7..bb1522c576 100644 --- a/packages/payload/src/utilities/createLocalReq.ts +++ b/packages/payload/src/utilities/createLocalReq.ts @@ -94,6 +94,7 @@ export const createLocalReq: CreateLocalReq = async ( req.user = user || req?.user || null req.payloadDataLoader = req?.payloadDataLoader || getDataLoader(req) req.routeParams = req?.routeParams || {} + req.query = req?.query || {} if (!req?.url) attachFakeURLProperties(req) diff --git a/packages/ui/src/elements/DeleteMany/index.tsx b/packages/ui/src/elements/DeleteMany/index.tsx index 3eb429d5cd..e45df78168 100644 --- a/packages/ui/src/elements/DeleteMany/index.tsx +++ b/packages/ui/src/elements/DeleteMany/index.tsx @@ -59,7 +59,7 @@ export const DeleteMany: React.FC = (props) => { toast.success(json.message || t('general:deletedSuccessfully'), { autoClose: 3000 }) toggleAll() dispatchSearchParams({ - type: 'set', + type: 'SET', browserHistory: 'replace', params: { page: selectAll ? '1' : undefined }, }) diff --git a/packages/ui/src/elements/EditMany/index.tsx b/packages/ui/src/elements/EditMany/index.tsx index 260d611280..84327237f9 100644 --- a/packages/ui/src/elements/EditMany/index.tsx +++ b/packages/ui/src/elements/EditMany/index.tsx @@ -154,7 +154,7 @@ export const EditMany: React.FC = (props) => { const onSuccess = () => { dispatchSearchParams({ - type: 'set', + type: 'SET', browserHistory: 'replace', params: { page: selectAll === SelectAllStatus.AllAvailable ? '1' : undefined }, }) diff --git a/packages/ui/src/elements/EditUpload/index.tsx b/packages/ui/src/elements/EditUpload/index.tsx index 8141555ac0..afca62e667 100644 --- a/packages/ui/src/elements/EditUpload/index.tsx +++ b/packages/ui/src/elements/EditUpload/index.tsx @@ -36,7 +36,7 @@ export const EditUpload: React.FC<{ }> = ({ fileName, fileSrc, imageCacheTag, showCrop, showFocalPoint }) => { const { closeModal } = useModal() const { t } = useTranslation() - const { formQueryParams, setFormQueryParams } = useFormQueryParams() + const { dispatchFormQueryParams, formQueryParams } = useFormQueryParams() const { uploadEdits } = formQueryParams || {} const [crop, setCrop] = useState({ height: uploadEdits?.crop?.height || 100, @@ -81,11 +81,16 @@ export const EditUpload: React.FC<{ } const saveEdits = () => { - setFormQueryParams({ - ...formQueryParams, - uploadEdits: { - crop: crop || undefined, - focalPoint: pointPosition ? pointPosition : undefined, + dispatchFormQueryParams({ + type: 'SET', + params: { + uploadEdits: + crop || pointPosition + ? { + crop: crop || null, + focalPoint: pointPosition ? pointPosition : null, + } + : null, }, }) closeModal(editDrawerSlug) diff --git a/packages/ui/src/elements/Localizer/index.tsx b/packages/ui/src/elements/Localizer/index.tsx index d8d029fa95..2ab10ed1df 100644 --- a/packages/ui/src/elements/Localizer/index.tsx +++ b/packages/ui/src/elements/Localizer/index.tsx @@ -49,7 +49,7 @@ const Localizer: React.FC<{ onClick={() => { close() dispatchSearchParams({ - type: 'set', + type: 'SET', params: { locale: searchParams.locale, }, diff --git a/packages/ui/src/elements/PublishMany/index.tsx b/packages/ui/src/elements/PublishMany/index.tsx index 365217311f..9063a77b42 100644 --- a/packages/ui/src/elements/PublishMany/index.tsx +++ b/packages/ui/src/elements/PublishMany/index.tsx @@ -64,7 +64,7 @@ export const PublishMany: React.FC = (props) => { if (res.status < 400) { toast.success(t('general:updatedSuccessfully')) dispatchSearchParams({ - type: 'set', + type: 'SET', browserHistory: 'replace', params: { page: selectAll ? '1' : undefined }, }) diff --git a/packages/ui/src/elements/UnpublishMany/index.tsx b/packages/ui/src/elements/UnpublishMany/index.tsx index 8efdec8e38..083748940a 100644 --- a/packages/ui/src/elements/UnpublishMany/index.tsx +++ b/packages/ui/src/elements/UnpublishMany/index.tsx @@ -61,7 +61,7 @@ export const UnpublishMany: React.FC = (props) => { if (res.status < 400) { toast.success(t('general:updatedSuccessfully')) dispatchSearchParams({ - type: 'set', + type: 'SET', browserHistory: 'replace', params: { page: selectAll ? '1' : undefined }, }) diff --git a/packages/ui/src/elements/Upload/index.tsx b/packages/ui/src/elements/Upload/index.tsx index ec72de8bb3..7731acb29b 100644 --- a/packages/ui/src/elements/Upload/index.tsx +++ b/packages/ui/src/elements/Upload/index.tsx @@ -9,7 +9,6 @@ import { useFormSubmitted } from '../../forms/Form/context' import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues' import { fieldBaseClass } from '../../forms/fields/shared' import useField from '../../forms/useField' -import { useClientFunctions } from '../../providers/ClientFunction' import { useDocumentInfo } from '../../providers/DocumentInfo' import { useTranslation } from '../../providers/Translation' import { Button } from '../Button' @@ -53,7 +52,6 @@ export const UploadActions = ({ canEdit, showSizePreviews }) => { export const Upload: React.FC = (props) => { const { collectionSlug, initialState, onChange, updatedAt, uploadConfig } = props - const clientFunctions = useClientFunctions() const submitted = useFormSubmitted() const [replacingFile, setReplacingFile] = useState(false) diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index f8c3ed232b..cb60b1f07d 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -6,11 +6,13 @@ import isDeepEqual from 'deep-equal' import { useRouter } from 'next/navigation' import { serialize } from 'object-to-formdata' import { wait } from 'payload/utilities' +import QueryString from 'qs' import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react' import { toast } from 'react-toastify' import type { Context as FormContextType, GetDataByPath, Props, SubmitOptions } from './types' +import { useFormQueryParams } from '../..' import { useDebouncedEffect } from '../../hooks/useDebouncedEffect' import useThrottledEffect from '../../hooks/useThrottledEffect' import { useAuth } from '../../providers/Auth' @@ -69,6 +71,7 @@ const Form: React.FC = (props) => { const { i18n, t } = useTranslation() const { refreshCookie, user } = useAuth() const operation = useOperation() + const { formQueryParams } = useFormQueryParams() const config = useConfig() const { @@ -221,7 +224,8 @@ const Form: React.FC = (props) => { let res if (typeof actionToUse === 'string') { - res = await requests[methodToUse.toLowerCase()](actionToUse, { + const actionEndpoint = `${actionToUse}${QueryString.stringify(formQueryParams, { addQueryPrefix: true })}` + res = await requests[methodToUse.toLowerCase()](actionEndpoint, { body: formData, headers: { 'Accept-Language': i18n.language, @@ -352,6 +356,7 @@ const Form: React.FC = (props) => { i18n, waitForAutocomplete, beforeSubmit, + formQueryParams, ], ) @@ -562,9 +567,14 @@ const Form: React.FC = (props) => { [fields, dispatchFields, onChange], ) + const actionString = + typeof action === 'string' + ? `${action}${QueryString.stringify(formQueryParams, { addQueryPrefix: true })}` + : '' + return (
void - }, -) +import type { Action, FormQueryParamsContext, State } from './types' + +import { useLocale } from '../Locale' + +export const FormQueryParams = createContext({} as FormQueryParamsContext) export const FormQueryParamsProvider: React.FC<{ children: React.ReactNode - formQueryParams?: QueryParamTypes - setFormQueryParams?: (params: QueryParamTypes) => void -}> = ({ children, formQueryParams: formQueryParamsFromProps }) => { - const [formQueryParams, setFormQueryParams] = useState( - formQueryParamsFromProps || ({} as QueryParamTypes), + initialParams?: State +}> = ({ children, initialParams: formQueryParamsFromProps }) => { + const [formQueryParams, dispatchFormQueryParams] = React.useReducer( + (state: State, action: Action) => { + const newState = { ...state } + + switch (action.type) { + case 'SET': + if (action.params?.uploadEdits === null && newState?.uploadEdits) { + delete newState.uploadEdits + } + if (action.params?.uploadEdits?.crop === null && newState?.uploadEdits?.crop) { + delete newState.uploadEdits.crop + } + if ( + action.params?.uploadEdits?.focalPoint === null && + newState?.uploadEdits?.focalPoint + ) { + delete newState.uploadEdits.focalPoint + } + return { + ...newState, + ...action.params, + } + default: + return state + } + }, + formQueryParamsFromProps || ({} as State), ) + const locale = useLocale() + + React.useEffect(() => { + dispatchFormQueryParams({ + type: 'SET', + params: { + locale: locale.code, + }, + }) + }, [locale.code]) + return ( - + {children} ) } export const useFormQueryParams = (): { - formQueryParams: QueryParamTypes - setFormQueryParams: (params: QueryParamTypes) => void + dispatchFormQueryParams: React.Dispatch + formQueryParams: State } => useContext(FormQueryParams) diff --git a/packages/ui/src/providers/FormQueryParams/types.ts b/packages/ui/src/providers/FormQueryParams/types.ts new file mode 100644 index 0000000000..49e1d605d5 --- /dev/null +++ b/packages/ui/src/providers/FormQueryParams/types.ts @@ -0,0 +1,18 @@ +import type { UploadEdits } from 'payload/types' + +export type FormQueryParamsContext = { + dispatchFormQueryParams: (action: Action) => void + formQueryParams: State +} + +export type State = { + depth: number + 'fallback-locale': string + locale: string + uploadEdits?: UploadEdits +} + +export type Action = { + params: Partial + type: 'SET' +} diff --git a/packages/ui/src/providers/Locale/index.tsx b/packages/ui/src/providers/Locale/index.tsx index a3c3629a6c..492baf5247 100644 --- a/packages/ui/src/providers/Locale/index.tsx +++ b/packages/ui/src/providers/Locale/index.tsx @@ -75,7 +75,7 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child useEffect(() => { if (searchParams?.locale) { dispatchSearchParams({ - type: 'set', + type: 'SET', params: { locale: searchParams.locale, }, diff --git a/packages/ui/src/providers/SearchParams/index.tsx b/packages/ui/src/providers/SearchParams/index.tsx index 71b6520a1c..370d7658c5 100644 --- a/packages/ui/src/providers/SearchParams/index.tsx +++ b/packages/ui/src/providers/SearchParams/index.tsx @@ -26,16 +26,16 @@ export const SearchParamsProvider: React.FC<{ children?: React.ReactNode }> = ({ let paramsToSet switch (action.type) { - case 'set': + case 'SET': paramsToSet = { ...state, ...action.params, } break - case 'replace': + case 'REPLACE': paramsToSet = action.params break - case 'clear': + case 'CLEAR': paramsToSet = {} break default: diff --git a/packages/ui/src/providers/SearchParams/types.ts b/packages/ui/src/providers/SearchParams/types.ts index 5b5b5ba8cf..d941d74d80 100644 --- a/packages/ui/src/providers/SearchParams/types.ts +++ b/packages/ui/src/providers/SearchParams/types.ts @@ -8,14 +8,14 @@ export type State = qs.ParsedQs export type Action = ( | { params: qs.ParsedQs - type: 'replace' + type: 'REPLACE' } | { params: qs.ParsedQs - type: 'set' + type: 'SET' } | { - type: 'clear' + type: 'CLEAR' } ) & { /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c33b1488d..1559b25bda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,6 +574,9 @@ importers: path-to-regexp: specifier: ^6.2.1 version: 6.2.1 + qs: + specifier: 6.11.2 + version: 6.11.2 react-diff-viewer-continued: specifier: 3.2.6 version: 3.2.6(react-dom@18.2.0)(react@18.2.0) @@ -1330,7 +1333,7 @@ importers: version: 2.3.0 next: specifier: 14.1.1-canary.26 - version: 14.1.1-canary.26(@babel/core@7.24.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1) + version: 14.1.1-canary.26(@babel/core@7.24.0)(react-dom@18.2.0)(react@18.2.0) object-to-formdata: specifier: 4.5.1 version: 4.5.1 @@ -13512,6 +13515,45 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: false + /next@14.1.1-canary.26(@babel/core@7.24.0)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vHj7hCL9qn8AhRXNEC1ujTO55w3IjckEE1tkmxwyqA3ypTH9PtxSnU6eFfC9C67Xf/Q2C5Btug7Yqvw7pxGkhg==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.1.1-canary.26 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001591 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.24.0)(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.1.1-canary.26 + '@next/swc-darwin-x64': 14.1.1-canary.26 + '@next/swc-linux-arm64-gnu': 14.1.1-canary.26 + '@next/swc-linux-arm64-musl': 14.1.1-canary.26 + '@next/swc-linux-x64-gnu': 14.1.1-canary.26 + '@next/swc-linux-x64-musl': 14.1.1-canary.26 + '@next/swc-win32-arm64-msvc': 14.1.1-canary.26 + '@next/swc-win32-ia32-msvc': 14.1.1-canary.26 + '@next/swc-win32-x64-msvc': 14.1.1-canary.26 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /next@14.1.1-canary.26(@babel/core@7.24.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1): resolution: {integrity: sha512-vHj7hCL9qn8AhRXNEC1ujTO55w3IjckEE1tkmxwyqA3ypTH9PtxSnU6eFfC9C67Xf/Q2C5Btug7Yqvw7pxGkhg==} engines: {node: '>=18.17.0'} diff --git a/test/_community/collections/Media/index.ts b/test/_community/collections/Media/index.ts index 16dd434e72..ed7f619eb4 100644 --- a/test/_community/collections/Media/index.ts +++ b/test/_community/collections/Media/index.ts @@ -4,7 +4,27 @@ export const mediaSlug = 'media' export const MediaCollection: CollectionConfig = { slug: mediaSlug, - upload: true, + upload: { + crop: true, + focalPoint: true, + imageSizes: [ + { + name: 'thumbnail', + width: 200, + height: 200, + }, + { + name: 'medium', + width: 800, + height: 800, + }, + { + name: 'large', + width: 1200, + height: 1200, + }, + ], + }, access: { read: () => true, create: () => true,