feat: 3.0 bulk edit parity (#5208)

This commit is contained in:
Jarrod Flesch
2024-02-29 10:51:02 -05:00
committed by GitHub
parent 972b8e110f
commit 203f105974
51 changed files with 585 additions and 468 deletions

View File

@@ -1,3 +0,0 @@
html {
color: blue;
}

View File

@@ -1,12 +0,0 @@
import React from 'react'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{/* Layout UI */}
<main>{children}</main>
</body>
</html>
)
}

View File

@@ -1,3 +0,0 @@
import './test.scss'
export default () => <h1>hello</h1>

View File

@@ -1,5 +0,0 @@
@import './another.scss';
html {
background: red;
}

View File

@@ -78,8 +78,9 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
collection.graphQL = {} as Collection['graphQL']
const hasIDField =
flattenTopLevelFields(fields).findIndex((field) => fieldAffectsData(field) && field.name === 'id') >
-1
flattenTopLevelFields(fields).findIndex(
(field) => fieldAffectsData(field) && field.name === 'id',
) > -1
const idType = getCollectionIDType(config.db.defaultIDType, collectionConfig)

View File

@@ -54,6 +54,7 @@
"dependencies": {
"@dnd-kit/core": "6.0.8",
"@faceless-ui/modal": "2.0.1",
"@faceless-ui/window-info": "2.1.1",
"@payloadcms/graphql": "workspace:*",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",

View File

@@ -96,7 +96,7 @@ export const Document = async ({
(isEditing && permissions?.collections?.[collectionSlug]?.update?.permission) ||
(!isEditing && permissions?.collections?.[collectionSlug]?.create?.permission)
apiURL = `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale}${
apiURL = `${serverURL}${api}/${collectionSlug}/${id}?locale=${locale.code}${
collectionConfig.versions?.drafts ? '&draft=true' : ''
}`
@@ -136,7 +136,7 @@ export const Document = async ({
hasSavePermission = isEditing && docPermissions?.update?.permission
action = `${serverURL}${api}/${globalSlug}`
apiURL = `${serverURL}${api}/${globalSlug}?locale=${locale}${
apiURL = `${serverURL}${api}/${globalSlug}?locale=${locale.code}${
globalConfig.versions?.drafts ? '&draft=true' : ''
}`
@@ -236,7 +236,7 @@ export const Document = async ({
initialData={data}
initialState={initialState}
/>
<EditDepthProvider depth={1} key={`${collectionSlug || globalSlug}-${locale}`}>
<EditDepthProvider depth={1} key={`${collectionSlug || globalSlug}-${locale.code}`}>
<FormQueryParamsProvider formQueryParams={formQueryParams}>
<RenderCustomComponent
CustomComponent={typeof CustomView === 'function' ? CustomView : undefined}

View File

@@ -41,7 +41,6 @@ export const DefaultEditView: React.FC = () => {
disableActions,
disableLeaveWithoutSaving,
docPermissions,
docPreferences,
globalSlug,
hasSavePermission,
initialData: data,
@@ -132,14 +131,15 @@ export const DefaultEditView: React.FC = () => {
apiRoute,
body: {
id,
docPreferences,
collectionSlug,
formState: prevFormState,
globalSlug,
operation,
schemaPath,
},
serverURL,
}),
[serverURL, apiRoute, id, operation, docPreferences, schemaPath],
[serverURL, apiRoute, id, operation, schemaPath, collectionSlug, globalSlug],
)
return (

View File

@@ -1,9 +1,11 @@
'use client'
import { useWindowInfo } from '@faceless-ui/window-info'
import { getTranslation } from '@payloadcms/translations'
import {
Button,
Gutter,
ListControls,
ListSelection,
Pagination,
PerPage,
Pill,
@@ -18,6 +20,10 @@ import {
} from '@payloadcms/ui'
import React, { Fragment, useEffect } from 'react'
import DeleteMany from '../../../../../ui/src/elements/DeleteMany'
import { EditMany } from '../../../../../ui/src/elements/EditMany'
import { PublishMany } from '../../../../../ui/src/elements/PublishMany'
import { UnpublishMany } from '../../../../../ui/src/elements/UnpublishMany'
import { RelationshipProvider } from './RelationshipProvider'
import './index.scss'
@@ -26,6 +32,7 @@ const baseClass = 'collection-list'
export const DefaultListView: React.FC = () => {
const {
Header,
collectionSlug,
data,
handlePageChange,
handlePerPageChange,
@@ -36,8 +43,6 @@ export const DefaultListView: React.FC = () => {
limit,
modifySearchParams,
newDocumentURL,
// resetParams,
collectionSlug,
titleField,
} = useListInfo()
@@ -58,6 +63,9 @@ export const DefaultListView: React.FC = () => {
const { i18n } = useTranslation()
const { setStepNav } = useStepNav()
const {
breakpoints: { s: smallBreak },
} = useWindowInfo()
let docs = data.docs || []
@@ -98,9 +106,9 @@ export const DefaultListView: React.FC = () => {
{i18n.t('general:createNew')}
</Pill>
)}
{/* {!smallBreak && (
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
)} */}
{!smallBreak && (
<ListSelection label={getTranslation(collectionConfig.labels.plural, i18n)} />
)}
{/* {description && (
<div className={`${baseClass}__sub-header`}>
<ViewDescription description={description} />
@@ -110,14 +118,12 @@ export const DefaultListView: React.FC = () => {
)}
</header>
<ListControls
collectionPluralLabel={labels?.plural}
collectionSlug={collectionSlug}
collectionConfig={collectionConfig}
// textFieldsToBeSearched={textFieldsToBeSearched}
// handleSearchChange={handleSearchChange}
// handleSortChange={handleSortChange}
// handleWhereChange={handleWhereChange}
// modifySearchQuery={modifySearchParams}
// resetParams={resetParams}
titleField={titleField}
/>
{BeforeListTable}
@@ -181,19 +187,21 @@ export const DefaultListView: React.FC = () => {
modifySearchParams={modifySearchParams}
resetPage={data.totalDocs <= data.pagingCounter}
/>
{/* {smallBreak && (
{smallBreak && (
<div className={`${baseClass}__list-selection`}>
<Fragment>
<ListSelection label={getTranslation(collection.labels.plural, i18n)} />
<ListSelection
label={getTranslation(collectionConfig.labels.plural, i18n)}
/>
<div className={`${baseClass}__list-selection-actions`}>
<EditMany resetParams={resetParams} />
<PublishMany resetParams={resetParams} />
<UnpublishMany resetParams={resetParams} />
<DeleteMany resetParams={resetParams} />
<EditMany collection={collectionConfig} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
<DeleteMany collection={collectionConfig} />
</div>
</Fragment>
</div>
)} */}
)}
</Fragment>
)}
</div>

View File

@@ -89,7 +89,7 @@ export const ListView = async ({
},
},
})
?.then((res) => res?.docs?.[0]?.value)) as unknown as ListPreferences
?.then((res) => res?.docs?.[0]?.value)) as ListPreferences
} catch (error) {}
const {
@@ -136,7 +136,11 @@ export const ListView = async ({
limit={limit}
newDocumentURL={`${admin}/collections/${collectionSlug}/create`}
>
<TableColumnsProvider collectionSlug={collectionSlug} listPreferences={listPreferences}>
<TableColumnsProvider
collectionSlug={collectionSlug}
enableRowSelections
listPreferences={listPreferences}
>
<RenderCustomComponent
CustomComponent={CustomListView}
DefaultComponent={DefaultListView}

View File

@@ -51,7 +51,7 @@ export const LoginForm: React.FC<{
disableSuccessStatus
initialState={initialState}
method="POST"
redirect={`${admin}${searchParams?.redirect || ''}`}
redirect={typeof searchParams?.redirect === 'string' ? searchParams.redirect : ''}
waitForAutocomplete
>
<FormLoadingOverlayToggle action="loading" name="login-form" />

View File

@@ -1,5 +1,5 @@
import type { BuildFormStateArgs, FieldSchemaMap } from '@payloadcms/ui'
import type { Field, PayloadRequest, SanitizedConfig } from 'payload/types'
import type { DocumentPreferences, Field, PayloadRequest, SanitizedConfig } from 'payload/types'
import { buildFieldSchemaMap, buildStateFromSchema, reduceFieldsToValues } from '@payloadcms/ui'
import httpStatus from 'http-status'
@@ -22,20 +22,21 @@ export const getFieldSchemaMap = (config: SanitizedConfig): FieldSchemaMap => {
}
export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
const { data: reqData, locale, t, user } = req
const { locale, t, user } = req
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
// TODO: run ADMIN access control for user
const fieldSchemaMap = getFieldSchemaMap(req.payload.config)
const {
id,
collectionSlug,
data: incomingData,
docPreferences,
formState,
globalSlug,
operation,
schemaPath,
} = reqData as BuildFormStateArgs
} = reqData
const schemaPathSegments = schemaPath.split('.')
@@ -64,6 +65,26 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }) => {
const data = incomingData || reduceFieldsToValues(formState, true)
let id: number | string | undefined
let docPreferencesKey: string
if (collectionSlug) {
id = reqData.id
docPreferencesKey = `collection-${collectionSlug}${id ? `-${id}` : ''}`
} else {
docPreferencesKey = `global-${globalSlug}`
}
const { docs: [{ value: docPreferences } = { value: null }] = [] } = (await req.payload.find({
collection: 'payload-preferences',
depth: 0,
limit: 1,
where: {
key: {
equals: docPreferencesKey,
},
},
})) as any as { docs: { value: DocumentPreferences }[] }
const result = await buildStateFromSchema({
id,
data,

View File

@@ -1,28 +0,0 @@
import httpStatus from 'http-status'
import { CollectionRouteHandler } from '../types'
import { BuildFormStateArgs, buildStateFromSchema, reduceFieldsToValues } from '@payloadcms/ui'
export const buildFormStateCollection: CollectionRouteHandler = async ({ req, collection }) => {
const { data: reqData, user, t, locale } = req
const { id, operation, docPreferences, formState } = reqData as BuildFormStateArgs
const data = reduceFieldsToValues(formState, true)
const result = await buildStateFromSchema({
id,
data,
fieldSchema: collection.config.fields,
locale,
operation,
preferences: docPreferences,
t,
user,
})
return Response.json(result, {
status: httpStatus.OK,
})
}

View File

@@ -1,26 +0,0 @@
import httpStatus from 'http-status'
import { GlobalRouteHandler } from '../types'
import { BuildFormStateArgs, buildStateFromSchema, reduceFieldsToValues } from '@payloadcms/ui'
export const buildFormStateGlobal: GlobalRouteHandler = async ({ req, globalConfig }) => {
const { data: reqData, user, t, locale } = req
const { docPreferences, formState } = reqData as BuildFormStateArgs
const data = reduceFieldsToValues(formState, true)
const result = await buildStateFromSchema({
data,
fieldSchema: globalConfig.fields,
locale,
preferences: docPreferences,
t,
user,
})
return Response.json(result, {
status: httpStatus.OK,
})
}

View File

@@ -326,7 +326,6 @@ export const POST =
break
case 2:
if (slug2 in endpoints.collection.POST) {
// /:collection/form-state
// /:collection/login
// /:collection/logout
// /:collection/unlock

View File

@@ -50,17 +50,17 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
})) || args
}, Promise.resolve())
const {
collection: { config: collectionConfig },
data,
disableEmail,
expiration,
req: {
payload: { config, emailOptions, sendEmail: email },
payload,
},
req,
} = args
const {
collection: { config: collectionConfig },
data,
disableEmail,
expiration,
req: {
payload: { config, emailOptions, sendEmail: email },
payload,
},
req,
} = args
// /////////////////////////////////////
// Forget password

View File

@@ -66,14 +66,15 @@ export const deleteOperation = async <TSlug extends keyof GeneratedTypes['collec
depth,
overrideAccess,
req: {
fallbackLocale,locale,
payload: { config },
payload,
},
req,
showHiddenFields,
where,
} = args
fallbackLocale,
locale,
payload: { config },
payload,
},
req,
showHiddenFields,
where,
} = args
if (!where) {
throw new APIError("Missing 'where' query of documents to delete.", httpStatus.BAD_REQUEST)

View File

@@ -63,7 +63,6 @@ export const deleteByIDOperation = async <TSlug extends keyof GeneratedTypes['co
locale,
payload: { config },
payload,
},
req,
showHiddenFields,

View File

@@ -80,7 +80,6 @@ export const updateByIDOperation = async <TSlug extends keyof GeneratedTypes['co
locale,
payload: { config },
payload,
},
req,
showHiddenFields,

View File

@@ -72,7 +72,7 @@ export const LinkButton: React.FC = () => {
const { closeModal, openModal } = useModal()
const drawerSlug = useDrawerSlug('rich-text-link')
const { id, getDocPreferences } = useDocumentInfo()
const { id, collectionSlug } = useDocumentInfo()
const { schemaPath } = useFieldPath()
const { richTextComponentMap } = fieldProps
@@ -94,13 +94,12 @@ export const LinkButton: React.FC = () => {
const data = {
text: editor.selection ? Editor.string(editor, editor.selection) : '',
}
const docPreferences = await getDocPreferences()
const state = await getFormState({
apiRoute: config.routes.api,
body: {
id,
collectionSlug,
data,
docPreferences,
operation: 'update',
schemaPath: `${schemaPath}.${linkFieldsSchemaPath}`,
},

View File

@@ -75,7 +75,7 @@ export const LinkElement = () => {
const [renderModal, setRenderModal] = useState(false)
const [renderPopup, setRenderPopup] = useState(false)
const [initialState, setInitialState] = useState<FormState>({})
const { id, getDocPreferences } = useDocumentInfo()
const { id, collectionSlug } = useDocumentInfo()
const drawerSlug = useDrawerSlug('rich-text-link')
@@ -96,14 +96,12 @@ export const LinkElement = () => {
url: element.url,
}
const docPreferences = await getDocPreferences()
const state = await getFormState({
apiRoute: config.routes.api,
body: {
id,
collectionSlug,
data,
docPreferences,
operation: 'update',
schemaPath: fieldMapPath,
},
@@ -114,7 +112,7 @@ export const LinkElement = () => {
}
void awaitInitialState()
}, [renderModal, element, user, locale, t, getDocPreferences, config, id, fieldMapPath])
}, [renderModal, element, user, locale, t, collectionSlug, config, id, fieldMapPath])
return (
<span className={baseClass} {...attributes}>

View File

@@ -45,7 +45,7 @@ export const UploadDrawer: React.FC<{
const { code: locale } = useLocale()
const { user } = useAuth()
const { closeModal } = useModal()
const { id, getDocPreferences } = useDocumentInfo()
const { id, collectionSlug } = useDocumentInfo()
const [initialState, setInitialState] = useState({})
const { richTextComponentMap } = fieldProps
@@ -72,14 +72,12 @@ export const UploadDrawer: React.FC<{
const data = deepCopyObject(element?.fields || {})
const awaitInitialState = async () => {
const docPreferences = await getDocPreferences()
const state = await getFormState({
apiRoute: config.routes.api,
body: {
id,
collectionSlug,
data,
docPreferences,
operation: 'update',
schemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${relatedCollection.slug}`,
},
@@ -89,14 +87,14 @@ export const UploadDrawer: React.FC<{
setInitialState(state)
}
awaitInitialState()
void awaitInitialState()
}, [
config,
element?.fields,
user,
locale,
t,
getDocPreferences,
collectionSlug,
id,
schemaPath,
relatedCollection.slug,

View File

@@ -8,18 +8,19 @@ import type { Props } from './types'
import { useAuth } from '../../providers/Auth'
import { useConfig } from '../../providers/Config'
import { useSearchParams } from '../../providers/SearchParams'
import { SelectAllStatus, useSelection } from '../../providers/SelectionProvider'
import { useTranslation } from '../../providers/Translation'
// import { requests } from '../../../api'
import { MinimalTemplate } from '../../templates/Minimal'
import { requests } from '../../utilities/api'
import { Button } from '../Button'
import Pill from '../Pill'
import './index.scss'
const baseClass = 'delete-documents'
const DeleteMany: React.FC<Props> = (props) => {
const { collection: { labels: { plural }, slug } = {}, resetParams } = props
export const DeleteMany: React.FC<Props> = (props) => {
const { collection: { slug, labels: { plural } } = {} } = props
const { permissions } = useAuth()
const {
@@ -30,6 +31,7 @@ const DeleteMany: React.FC<Props> = (props) => {
const { count, getQueryParams, selectAll, toggleAll } = useSelection()
const { i18n, t } = useTranslation()
const [deleting, setDeleting] = useState(false)
const { dispatchSearchParams } = useSearchParams()
const collectionPermissions = permissions?.collections?.[slug]
const hasDeletePermission = collectionPermissions?.delete?.permission
@@ -40,37 +42,54 @@ const DeleteMany: React.FC<Props> = (props) => {
toast.error(t('error:unknown'))
}, [t])
const handleDelete = useCallback(() => {
const handleDelete = useCallback(async () => {
setDeleting(true)
// requests
// .delete(`${serverURL}${api}/${slug}${getQueryParams()}`, {
// headers: {
// 'Accept-Language': i18n.language,
// 'Content-Type': 'application/json',
// },
// })
// .then(async (res) => {
// try {
// const json = await res.json()
// toggleModal(modalSlug)
// if (res.status < 400) {
// toast.success(json.message || t('general:deletedSuccessfully'), { autoClose: 3000 })
// toggleAll()
// resetParams({ page: selectAll ? 1 : undefined })
// return null
// }
await requests
.delete(`${serverURL}${api}/${slug}${getQueryParams()}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
})
.then(async (res) => {
try {
const json = await res.json()
toggleModal(modalSlug)
if (res.status < 400) {
toast.success(json.message || t('general:deletedSuccessfully'), { autoClose: 3000 })
toggleAll()
dispatchSearchParams({
type: 'set',
browserHistory: 'replace',
params: { page: selectAll ? '1' : undefined },
})
return null
}
// if (json.errors) {
// toast.error(json.message)
// } else {
// addDefaultError()
// }
// return false
// } catch (e) {
// return addDefaultError()
// }
// })
}, [])
if (json.errors) {
toast.error(json.message)
} else {
addDefaultError()
}
return false
} catch (e) {
return addDefaultError()
}
})
}, [
addDefaultError,
api,
dispatchSearchParams,
getQueryParams,
i18n.language,
modalSlug,
selectAll,
serverURL,
slug,
t,
toggleAll,
toggleModal,
])
if (selectAll === SelectAllStatus.None || !hasDeletePermission) {
return null

View File

@@ -2,6 +2,5 @@ import type { SanitizedCollectionConfig } from 'payload/types'
export type Props = {
collection: SanitizedCollectionConfig
resetParams: () => void
title?: string
}

View File

@@ -1,5 +1,4 @@
'use client'
import type { CollectionPermission } from 'payload/auth'
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
@@ -71,7 +70,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
useEffect(() => {
setFields(formatFields(fields, true))
}, [collectionSlug, collectionConfig])
}, [collectionSlug, collectionConfig, fields])
useEffect(() => {
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen))
@@ -84,11 +83,9 @@ const Content: React.FC<DocumentDrawerProps> = ({
}
}, [isError, t, isOpen, data, drawerSlug, closeModal, isLoadingDocument])
if (isError) return null
const isEditing = Boolean(id)
const apiURL = id ? `${serverURL}${apiRoute}/${collectionSlug}/${id}?locale=${locale}` : null
const apiURL = id ? `${serverURL}${apiRoute}/${collectionSlug}/${id}?locale=${locale.code}` : null
const action = `${serverURL}${apiRoute}/${collectionSlug}${
isEditing ? `/${id}` : ''
@@ -105,8 +102,8 @@ const Content: React.FC<DocumentDrawerProps> = ({
apiRoute,
body: {
id,
collectionSlug,
data: data || {},
docPreferences: null, // TODO: get this
operation: isEditing ? 'update' : 'create',
schemaPath,
},
@@ -114,11 +111,14 @@ const Content: React.FC<DocumentDrawerProps> = ({
})
setInitialState(result)
hasInitializedState.current = true
}
getInitialState()
void getInitialState()
}
}, [apiRoute, data, id, isEditing, schemaPath, serverURL])
}, [apiRoute, data, id, isEditing, schemaPath, serverURL, collectionSlug])
if (isError) return null
if (!initialState || isLoadingDocument) {
return <LoadingOverlay />
@@ -142,6 +142,7 @@ const Content: React.FC<DocumentDrawerProps> = ({
aria-label={t('general:close')}
className={`${baseClass}__header-close`}
onClick={() => toggleModal(drawerSlug)}
type="button"
>
<X />
</button>
@@ -154,11 +155,9 @@ const Content: React.FC<DocumentDrawerProps> = ({
collectionSlug={collectionConfig.slug}
disableActions
disableLeaveWithoutSaving
docPermissions={{} as CollectionPermission} // TODO; get this
// hasSavePermission={hasSavePermission}
// isEditing={isEditing}
// isLoading,
docPreferences={null} // TODO: get this
id={id}
initialData={data}
initialState={initialState}

View File

@@ -3,10 +3,13 @@ import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import React, { useCallback, useState } from 'react'
import type { FormState } from '../..'
import type { Props } from './types'
import { DocumentInfoProvider, FieldPathProvider, getFormState, useComponentMap } from '../..'
import Form from '../../forms/Form'
import { useForm } from '../../forms/Form/context'
import RenderFields from '../../forms/RenderFields'
import FormSubmit from '../../forms/Submit'
import { X } from '../../icons/X'
import { useAuth } from '../../providers/Auth'
@@ -81,25 +84,66 @@ const SaveDraft: React.FC<{ action: string; disabled: boolean }> = ({ action, di
</FormSubmit>
)
}
const EditMany: React.FC<Props> = (props) => {
export const EditMany: React.FC<Props> = (props) => {
const { collection: { slug, fields, labels: { plural } } = {}, collection } = props
const { permissions } = useAuth()
const { closeModal } = useModal()
const {
routes: { api },
routes: { api: apiRoute },
serverURL,
} = useConfig()
const { count, getQueryParams, selectAll } = useSelection()
const { i18n, t } = useTranslation()
const [selected, setSelected] = useState([])
const { dispatchSearchParams } = useSearchParams()
const { componentMap } = useComponentMap()
const [reducedFieldMap, setReducedFieldMap] = useState([])
const [initialState, setInitialState] = useState<FormState>()
const hasInitializedState = React.useRef(false)
const collectionPermissions = permissions?.collections?.[slug]
const hasUpdatePermission = collectionPermissions?.update?.permission
const drawerSlug = `edit-${slug}`
React.useEffect(() => {
if (componentMap?.collections?.[slug]?.fieldMap) {
const fieldMap = componentMap.collections[slug].fieldMap
const reducedFieldMap = []
fieldMap.map((field) => {
selected.map((selectedField) => {
if (field.name === selectedField.name) {
reducedFieldMap.push(field)
}
})
})
setReducedFieldMap(reducedFieldMap)
}
}, [componentMap.collections, fields, slug, selected])
React.useEffect(() => {
if (!hasInitializedState.current) {
const getInitialState = async () => {
const result = await getFormState({
apiRoute,
body: {
collectionSlug: slug,
data: {},
operation: 'update',
schemaPath: slug,
},
serverURL,
})
setInitialState(result)
hasInitializedState.current = true
}
void getInitialState()
}
}, [apiRoute, hasInitializedState, serverURL, slug])
if (selectAll === SelectAllStatus.None || !hasUpdatePermission) {
return null
}
@@ -128,64 +172,62 @@ const EditMany: React.FC<Props> = (props) => {
{/* @ts-expect-error */}
<DocumentInfoProvider collection={collection}>
<OperationContext.Provider value="update">
<Form className={`${baseClass}__form`} onSuccess={onSuccess}>
<div className={`${baseClass}__main`}>
<div className={`${baseClass}__header`}>
<h2 className={`${baseClass}__header__title`}>
{t('general:editingLabel', { count, label: getTranslation(plural, i18n) })}
</h2>
<button
aria-label={t('general:close')}
className={`${baseClass}__header__close`}
id={`close-drawer__${drawerSlug}`}
onClick={() => closeModal(drawerSlug)}
type="button"
>
<X />
</button>
</div>
<FieldSelect fields={fields} setSelected={setSelected} />
[RenderFields]
{/* <RenderFields fieldSchema={selected} fieldTypes={fieldTypes} /> */}
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__document-actions`}>
{collection.versions ? (
<React.Fragment>
<Publish
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
<SaveDraft
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
</React.Fragment>
) : (
<React.Fragment>
<div className={`${baseClass}__main`}>
<div className={`${baseClass}__header`}>
<h2 className={`${baseClass}__header__title`}>
{t('general:editingLabel', { count, label: getTranslation(plural, i18n) })}
</h2>
<button
aria-label={t('general:close')}
className={`${baseClass}__header__close`}
id={`close-drawer__${drawerSlug}`}
onClick={() => closeModal(drawerSlug)}
type="button"
>
<X />
</button>
</div>
<FieldPathProvider path="" schemaPath={slug}>
<Form
className={`${baseClass}__form`}
initialState={initialState}
onSuccess={onSuccess}
>
<FieldSelect fields={fields} setSelected={setSelected} />
{reducedFieldMap.length === 0 ? null : (
<RenderFields fieldMap={reducedFieldMap} />
)}
<div className={`${baseClass}__sidebar-wrap`}>
<div className={`${baseClass}__sidebar`}>
<div className={`${baseClass}__sidebar-sticky-wrap`}>
<div className={`${baseClass}__document-actions`}>
{collection?.versions?.drafts ? (
<React.Fragment>
<Publish
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
<SaveDraft
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
</React.Fragment>
) : (
<Submit
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
action={`${serverURL}${apiRoute}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
<SaveDraft
action={`${serverURL}${api}/${slug}${getQueryParams()}`}
disabled={selected.length === 0}
/>
</React.Fragment>
)}
)}
</div>
</div>
</div>
</div>
</div>
</div>
</Form>
</Form>
</FieldPathProvider>
</div>
</OperationContext.Provider>
{/* @ts-expect-error */}
</DocumentInfoProvider>
</Drawer>
</div>
)
}
export default EditMany

View File

@@ -87,7 +87,7 @@ export const FieldSelect: React.FC<Props> = ({ fields, setSelected }) => {
setSelected(selected.map(({ value }) => value))
}
// remove deselected values from form state
if (selected === null || Object.keys(activeFields).length > selected.length) {
if (selected === null || Object.keys(activeFields || []).length > selected.length) {
Object.keys(activeFields).forEach((path) => {
if (
selected === null ||
@@ -96,8 +96,8 @@ export const FieldSelect: React.FC<Props> = ({ fields, setSelected }) => {
})
) {
dispatchFields({
path,
type: 'REMOVE',
path,
})
}
})

View File

@@ -12,13 +12,13 @@ import { useSearchParams } from '../../providers/SearchParams'
import { useTranslation } from '../../providers/Translation'
import { Button } from '../Button'
import ColumnSelector from '../ColumnSelector'
import DeleteMany from '../DeleteMany'
import EditMany from '../EditMany'
import { DeleteMany } from '../DeleteMany'
import { EditMany } from '../EditMany'
import Pill from '../Pill'
import PublishMany from '../PublishMany'
import { PublishMany } from '../PublishMany'
import SearchFilter from '../SearchFilter'
import SortComplex from '../SortComplex'
import UnpublishMany from '../UnpublishMany'
import { UnpublishMany } from '../UnpublishMany'
import WhereBuilder from '../WhereBuilder'
import validateWhereQuery from '../WhereBuilder/validateWhereQuery'
import './index.scss'
@@ -32,8 +32,7 @@ const baseClass = 'list-controls'
*/
export const ListControls: React.FC<Props> = (props) => {
const {
collectionPluralLabel,
collectionSlug,
collectionConfig,
enableColumns = true,
enableSort = false,
handleSearchChange,
@@ -71,10 +70,10 @@ export const ListControls: React.FC<Props> = (props) => {
<div className={`${baseClass}__buttons-wrap`}>
{!smallBreak && (
<React.Fragment>
{/* <EditMany resetParams={resetParams} />
<PublishMany resetParams={resetParams} />
<UnpublishMany resetParams={resetParams} />
<DeleteMany resetParams={resetParams} /> */}
<EditMany collection={collectionConfig} />
<PublishMany collection={collectionConfig} />
<UnpublishMany collection={collectionConfig} />
<DeleteMany collection={collectionConfig} />
</React.Fragment>
)}
{enableColumns && (
@@ -126,7 +125,7 @@ export const ListControls: React.FC<Props> = (props) => {
height={visibleDrawer === 'columns' ? 'auto' : 0}
id={`${baseClass}-columns`}
>
<ColumnSelector collectionSlug={collectionSlug} />
<ColumnSelector collectionSlug={collectionConfig.slug} />
</AnimateHeight>
)}
<AnimateHeight
@@ -135,8 +134,8 @@ export const ListControls: React.FC<Props> = (props) => {
id={`${baseClass}-where`}
>
<WhereBuilder
collectionPluralLabel={collectionPluralLabel}
collectionSlug={collectionSlug}
collectionPluralLabel={collectionConfig?.labels?.plural}
collectionSlug={collectionConfig.slug}
handleChange={handleWhereChange}
modifySearchQuery={modifySearchQuery}
/>

View File

@@ -3,8 +3,7 @@ import type { FieldAffectingData, SanitizedCollectionConfig, Where } from 'paylo
import type { Column } from '../Table/types'
export type Props = {
collectionPluralLabel: SanitizedCollectionConfig['labels']['plural']
collectionSlug: SanitizedCollectionConfig['slug']
collectionConfig: SanitizedCollectionConfig
enableColumns?: boolean
enableSort?: boolean
handleSearchChange?: (search: string) => void

View File

@@ -1,3 +1,4 @@
'use client'
import React, { Fragment } from 'react'
import { SelectAllStatus, useSelection } from '../../providers/SelectionProvider'
@@ -9,7 +10,7 @@ const baseClass = 'list-selection'
type Props = {
label: string
}
const ListSelection: React.FC<Props> = ({ label }) => {
export const ListSelection: React.FC<Props> = ({ label }) => {
const { count, selectAll, toggleAll, totalDocs } = useSelection()
const { t } = useTranslation()
@@ -37,5 +38,3 @@ const ListSelection: React.FC<Props> = ({ label }) => {
</div>
)
}
export default ListSelection

View File

@@ -19,7 +19,7 @@ import './index.scss'
const baseClass = 'publish-many'
const PublishMany: React.FC<Props> = (props) => {
export const PublishMany: React.FC<Props> = (props) => {
const { collection: { slug, labels: { plural }, versions } = {} } = props
const {
@@ -130,5 +130,3 @@ const PublishMany: React.FC<Props> = (props) => {
</React.Fragment>
)
}
export default PublishMany

View File

@@ -1,12 +1,13 @@
'use client'
import { CheckboxInput, SelectAllStatus, useSelection, useTranslation } from '@payloadcms/ui'
import React from 'react'
import { CheckboxInput, SelectAllStatus, useSelection, useTranslation } from '../..'
import './index.scss'
const baseClass = 'select-all'
const SelectAll: React.FC = () => {
export const SelectAll: React.FC = () => {
const { selectAll, toggleAll } = useSelection()
const { i18n } = useTranslation()
@@ -22,11 +23,9 @@ const SelectAll: React.FC = () => {
}
className={`${baseClass}__checkbox`}
id="select-all"
onChange={() => toggleAll()}
name="select-all"
onToggle={() => toggleAll()}
partialChecked={selectAll === SelectAllStatus.Some}
path="select-all"
/>
)
}
export default SelectAll

View File

@@ -1,21 +1,20 @@
'use client'
import { CheckboxInput, useSelection } from '@payloadcms/ui'
import React from 'react'
import { CheckboxInput, useSelection, useTableCell } from '../..'
import './index.scss'
const baseClass = 'select-row'
const SelectRow: React.FC<{ id: number | string }> = ({ id }) => {
export const SelectRow: React.FC = () => {
const { selected, setSelection } = useSelection()
const { rowData } = useTableCell()
return (
<CheckboxInput
checked={selected[id]}
// onToggle={() => setSelection(id)}
checked={selected?.[rowData?.id]}
className={`${baseClass}__checkbox`}
onToggle={() => setSelection(rowData.id)}
/>
)
}
export default SelectRow

View File

@@ -1,17 +1,28 @@
import type { CellProps, SanitizedCollectionConfig } from 'payload/types'
import type { Column } from '../../elements/Table/types'
import type { ColumnPreferences } from '../../providers/ListInfo/types'
import type { FieldMap } from '../../utilities/buildComponentMap/types'
import type { Column } from '../Table/types'
import { SelectAll } from '../SelectAll'
import { SelectRow } from '../SelectRow'
export const buildColumns = (args: {
cellProps: Partial<CellProps>[]
columnPreferences: ColumnPreferences
defaultColumns?: string[]
enableRowSelections: boolean
fieldMap: FieldMap
useAsTitle: SanitizedCollectionConfig['admin']['useAsTitle']
}): Column[] => {
const { cellProps, columnPreferences, defaultColumns, fieldMap, useAsTitle } = args
const {
cellProps,
columnPreferences,
defaultColumns,
enableRowSelections,
fieldMap,
useAsTitle,
} = args
let sortedFieldMap = fieldMap
@@ -32,7 +43,7 @@ export const buildColumns = (args: {
let numberOfActiveColumns = 0
return sortedFieldMap.reduce((acc, field, index) => {
const sorted = sortedFieldMap.reduce((acc, field, index) => {
const columnPreference = columnPreferences?.find(
(preference) => preference.accessor === field.name,
)
@@ -56,7 +67,10 @@ export const buildColumns = (args: {
name: field.name,
accessor: field.name,
active,
cellProps: cellProps?.[index],
cellProps: {
...cellProps?.[index],
link: (numberOfActiveColumns === 1 && active && enableRowSelections) || undefined,
},
components: {
Cell: field.Cell,
Heading: field.Heading,
@@ -69,4 +83,19 @@ export const buildColumns = (args: {
return acc
}, [])
if (enableRowSelections) {
sorted.unshift({
name: '',
accessor: '_select',
active: true,
components: {
Cell: <SelectRow />,
Heading: <SelectAll />,
},
label: null,
})
}
return sorted
}

View File

@@ -34,8 +34,9 @@ export const TableColumnsProvider: React.FC<{
cellProps?: Partial<CellProps>[]
children: React.ReactNode
collectionSlug: string
enableRowSelections?: boolean
listPreferences: ListPreferences
}> = ({ cellProps, children, collectionSlug, listPreferences }) => {
}> = ({ cellProps, children, collectionSlug, enableRowSelections = false, listPreferences }) => {
const config = useConfig()
const { componentMap } = useComponentMap()
@@ -60,6 +61,7 @@ export const TableColumnsProvider: React.FC<{
cellProps,
columnPreferences: listPreferences?.columns,
defaultColumns,
enableRowSelections,
fieldMap,
useAsTitle,
})
@@ -87,6 +89,7 @@ export const TableColumnsProvider: React.FC<{
cellProps,
columnPreferences: currentPreferences?.columns,
defaultColumns,
enableRowSelections: true,
fieldMap,
useAsTitle,
}),
@@ -159,14 +162,17 @@ export const TableColumnsProvider: React.FC<{
[dispatchTableColumns],
)
const toggleColumn = useCallback((column: string) => {
dispatchTableColumns({
type: 'toggle',
payload: {
column,
},
})
}, [])
const toggleColumn = useCallback(
(column: string) => {
dispatchTableColumns({
type: 'toggle',
payload: {
column,
},
})
},
[dispatchTableColumns],
)
return (
<TableColumnContext.Provider

View File

@@ -8,18 +8,19 @@ import type { Props } from './types'
import { useAuth } from '../../providers/Auth'
import { useConfig } from '../../providers/Config'
import { useSearchParams } from '../../providers/SearchParams'
import { SelectAllStatus, useSelection } from '../../providers/SelectionProvider'
import { useTranslation } from '../../providers/Translation'
// import { requests } from '../../../api'
import { MinimalTemplate } from '../../templates/Minimal'
import { requests } from '../../utilities/api'
import { Button } from '../Button'
import Pill from '../Pill'
import './index.scss'
const baseClass = 'unpublish-many'
const UnpublishMany: React.FC<Props> = (props) => {
const { collection: { labels: { plural }, slug, versions } = {}, resetParams } = props
export const UnpublishMany: React.FC<Props> = (props) => {
const { collection: { slug, labels: { plural }, versions } = {} } = props
const {
routes: { api },
@@ -28,8 +29,9 @@ const UnpublishMany: React.FC<Props> = (props) => {
const { permissions } = useAuth()
const { toggleModal } = useModal()
const { i18n, t } = useTranslation()
const { count, getQueryParams, selectAll } = useSelection()
const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false)
const { dispatchSearchParams } = useSearchParams()
const collectionPermissions = permissions?.collections?.[slug]
const hasPermission = collectionPermissions?.update?.permission
@@ -40,45 +42,49 @@ const UnpublishMany: React.FC<Props> = (props) => {
toast.error(t('error:unknown'))
}, [t])
const handleUnpublish = useCallback(() => {
const handleUnpublish = useCallback(async () => {
setSubmitted(true)
// requests
// .patch(`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'draft' } })}`, {
// body: JSON.stringify({
// _status: 'draft',
// }),
// headers: {
// 'Accept-Language': i18n.language,
// 'Content-Type': 'application/json',
// },
// })
// .then(async (res) => {
// try {
// const json = await res.json()
// toggleModal(modalSlug)
// if (res.status < 400) {
// toast.success(t('general:updatedSuccessfully'))
// resetParams({ page: selectAll ? 1 : undefined })
// return null
// }
await requests
.patch(`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'draft' } })}`, {
body: JSON.stringify({
_status: 'draft',
}),
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
})
.then(async (res) => {
try {
const json = await res.json()
toggleModal(modalSlug)
if (res.status < 400) {
toast.success(t('general:updatedSuccessfully'))
dispatchSearchParams({
type: 'set',
browserHistory: 'replace',
params: { page: selectAll ? '1' : undefined },
})
return null
}
// if (json.errors) {
// json.errors.forEach((error) => toast.error(error.message))
// } else {
// addDefaultError()
// }
// return false
// } catch (e) {
// return addDefaultError()
// }
// })
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return false
} catch (e) {
return addDefaultError()
}
})
}, [
addDefaultError,
api,
dispatchSearchParams,
getQueryParams,
i18n.language,
modalSlug,
resetParams,
selectAll,
serverURL,
slug,
@@ -121,5 +127,3 @@ const UnpublishMany: React.FC<Props> = (props) => {
</React.Fragment>
)
}
export default UnpublishMany

View File

@@ -2,5 +2,4 @@ import type { SanitizedCollectionConfig } from 'payload/types'
export type Props = {
collection: SanitizedCollectionConfig
resetParams: () => void
}

View File

@@ -15,6 +15,7 @@ export { HydrateClientUser } from '../elements/HydrateClientUser'
export { LeaveWithoutSaving } from '../elements/LeaveWithoutSaving'
export { ListControls } from '../elements/ListControls'
export { useListDrawer } from '../elements/ListDrawer'
export { ListSelection } from '../elements/ListSelection'
export { LoadingOverlayToggle } from '../elements/Loading'
export { FormLoadingOverlayToggle } from '../elements/Loading'
export { LoadingOverlay } from '../elements/Loading'

View File

@@ -23,7 +23,7 @@ export { default as Submit } from '../forms/Submit'
export { fieldTypes } from '../forms/fields'
export { default as SectionTitle } from '../forms/fields/Blocks/SectionTitle'
export { default as Checkbox } from '../forms/fields/Checkbox'
export { default as CheckboxInput } from '../forms/fields/Checkbox'
export { CheckboxInput } from '../forms/fields/Checkbox/Input'
export { default as ConfirmPassword } from '../forms/fields/ConfirmPassword'
export { default as Email } from '../forms/fields/Email'
export { default as HiddenInput } from '../forms/fields/HiddenInput'

View File

@@ -0,0 +1,74 @@
'use client'
import React from 'react'
import { Check, Line } from '../../..'
type Props = {
AfterInput?: React.ReactNode
BeforeInput?: React.ReactNode
Label?: React.ReactNode
checked?: boolean
className?: string
id?: string
inputRef?: React.RefObject<HTMLInputElement>
name?: string
onToggle: (event: React.ChangeEvent<HTMLInputElement>) => void
partialChecked?: boolean
readOnly?: boolean
required?: boolean
}
export const inputBaseClass = 'checkbox-input'
export const CheckboxInput: React.FC<Props> = ({
id,
name,
AfterInput,
BeforeInput,
Label,
checked,
className,
inputRef,
onToggle,
partialChecked,
readOnly,
required,
}) => {
return (
<div
className={[
className,
inputBaseClass,
checked && `${inputBaseClass}--checked`,
readOnly && `${inputBaseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${inputBaseClass}__input`}>
{BeforeInput}
<input
aria-label=""
defaultChecked={Boolean(checked)}
disabled={readOnly}
id={id}
name={name}
onInput={onToggle}
ref={inputRef}
required={required}
type="checkbox"
/>
<span
className={[`${inputBaseClass}__icon`, !checked && partialChecked ? 'check' : 'partial']
.filter(Boolean)
.join(' ')}
>
{checked && <Check />}
{!checked && partialChecked && <Line />}
</span>
{AfterInput}
</div>
{Label}
</div>
)
}

View File

@@ -5,18 +5,15 @@ import React, { useCallback } from 'react'
import type { Props } from './types'
import { Check } from '../../../icons/Check'
import { Line } from '../../../icons/Line'
import LabelComp from '../../Label'
import useField from '../../useField'
import { withCondition } from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { CheckboxInput } from './Input'
import './index.scss'
const baseClass = 'checkbox'
export const inputBaseClass = 'checkbox-input'
const Checkbox: React.FC<Props> = (props) => {
const {
id,
@@ -86,40 +83,19 @@ const Checkbox: React.FC<Props> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>{Error}</div>
<div
className={[
inputBaseClass,
checked && `${inputBaseClass}--checked`,
readOnly && `${inputBaseClass}--read-only`,
]
.filter(Boolean)
.join(' ')}
>
<div className={`${inputBaseClass}__input`}>
{BeforeInput}
<input
aria-label=""
defaultChecked={Boolean(checked)}
disabled={readOnly}
id={fieldID}
name={path}
onInput={onToggle}
required={required}
// ref={inputRef}
type="checkbox"
/>
<span
className={[`${inputBaseClass}__icon`, !value && partialChecked ? 'check' : 'partial']
.filter(Boolean)
.join(' ')}
>
{value && <Check />}
{!value && partialChecked && <Line />}
</span>
{AfterInput}
</div>
{Label}
</div>
<CheckboxInput
AfterInput={AfterInput}
BeforeInput={BeforeInput}
Label={Label}
checked={checked}
id={fieldID}
inputRef={null}
name={path}
onToggle={onToggle}
partialChecked={partialChecked}
readOnly={readOnly}
required={required}
/>
{Description}
</div>
)

View File

@@ -1,7 +1,7 @@
import type { TFunction } from '@payloadcms/translations'
import type { User } from 'payload/auth'
import type { Locale } from 'payload/config'
import type { Data, DocumentPreferences, Field as FieldSchema } from 'payload/types'
import type { Data, Field as FieldSchema } from 'payload/types'
import type { FormState } from '../../Form/types'
@@ -22,9 +22,10 @@ type Args = {
}
export type BuildFormStateArgs = {
collectionSlug?: string
data?: Data
docPreferences: DocumentPreferences
formState?: FormState
globalSlug?: string
id?: number | string
operation?: 'create' | 'update'
schemaPath: string

View File

@@ -18,6 +18,13 @@ const Context = createContext({} as DocumentInfoContext)
export const useDocumentInfo = (): DocumentInfoContext => useContext(Context)
/**
* To initialize documentInfo from the server
* use the <SetDocumentInfo /> within a RSC component
* to hydrate the documentInfo on the first render.
*
* Otherwise pass props to initialize the documentInfo.
**/
export const DocumentInfoProvider: React.FC<
DocumentInfoProps & {
children: React.ReactNode
@@ -53,6 +60,13 @@ export const DocumentInfoProvider: React.FC<
const [title, setTitle] = useState<string>('')
const setDocumentTitle = useCallback<DocumentInfoContext['setDocumentTitle']>(
(title) => {
setTitle(title || id?.toString() || '[untitled]')
},
[id],
)
const baseURL = `${serverURL}${api}`
let slug: string
let pluralType: 'collections' | 'globals'
@@ -259,19 +273,26 @@ export const DocumentInfoProvider: React.FC<
)
useEffect(() => {
getVersions()
void getVersions()
}, [getVersions])
useEffect(() => {
getDocPermissions()
}, [getDocPermissions])
const loadDocPermissions = async () => {
const docPermissions: DocumentPermissions = rest.docPermissions
if (!docPermissions) await getDocPermissions()
else setDocPermissions(docPermissions)
}
void loadDocPermissions()
}, [getDocPermissions, rest.docPermissions, setDocPermissions])
const setDocumentTitle = useCallback<DocumentInfoContext['setDocumentTitle']>(
(title) => {
setTitle(title || id?.toString() || '[untitled]')
},
[id],
)
useEffect(() => {
const loadDocPreferences = async () => {
let docPreferences: DocumentPreferences = rest.docPreferences
if (!docPreferences) docPreferences = await getDocPreferences()
void setPreference(preferencesKey, docPreferences)
}
void loadDocPreferences()
}, [getDocPreferences, preferencesKey, rest.docPreferences, setPreference])
const value: DocumentInfoContext = {
...documentInfo,

View File

@@ -43,7 +43,7 @@ export type DocumentInfo = DocumentInfoProps & {
versionsCount?: PaginatedDocs<TypeWithVersion<any>>
}
export type DocumentInfoContext = DocumentInfo & {
export type DocumentInfoContext = Omit<DocumentInfo, 'docPreferences'> & {
getDocPermissions: () => Promise<void>
getDocPreferences: () => Promise<{ [key: string]: unknown }>
getVersions: () => Promise<void>

3
pnpm-lock.yaml generated
View File

@@ -520,6 +520,9 @@ importers:
'@faceless-ui/modal':
specifier: 2.0.1
version: 2.0.1(react-dom@18.2.0)(react@18.2.0)
'@faceless-ui/window-info':
specifier: 2.1.1
version: 2.1.1(react-dom@18.2.0)(react@18.2.0)
'@payloadcms/graphql':
specifier: workspace:*
version: link:../graphql

View File

@@ -56,8 +56,8 @@ export const CollectionArchive: React.FC<Props> = props => {
docs: (populateBy === 'collection'
? populatedDocs
: populateBy === 'selection'
? selectedDocs
: []
? selectedDocs
: []
)?.map(doc => doc.value),
hasNextPage: false,
hasPrevPage: false,

View File

@@ -56,8 +56,8 @@ export const CollectionArchive: React.FC<Props> = props => {
docs: (populateBy === 'collection'
? populatedDocs
: populateBy === 'selection'
? selectedDocs
: []
? selectedDocs
: []
)?.map(doc => doc.value),
hasNextPage: false,
hasPrevPage: false,

View File

@@ -3,9 +3,9 @@ import * as AWS from '@aws-sdk/client-s3'
import path from 'path'
import type { Payload } from '../../packages/payload/src'
import { describeIfInCIOrHasLocalstack } from '../helpers'
import { getPayload } from '../../packages/payload/src'
import { describeIfInCIOrHasLocalstack } from '../helpers'
import { startMemoryDB } from '../startMemoryDB'
import configPromise from './config'
@@ -16,103 +16,104 @@ describe('@payloadcms/plugin-cloud-storage', () => {
const config = await startMemoryDB(configPromise)
payload = await getPayload({ config })
})
const TEST_BUCKET = 'payload-bucket'
const TEST_BUCKET = 'payload-bucket'
let client: AWS.S3Client
describeIfInCIOrHasLocalstack()('plugin-cloud-storage', () => {
describe('S3', () => {
beforeAll(async () => {
client = new AWS.S3({
endpoint: 'http://localhost:4566',
region: 'us-east-1',
forcePathStyle: true, // required for localstack
let client: AWS.S3Client
describeIfInCIOrHasLocalstack()('plugin-cloud-storage', () => {
describe('S3', () => {
beforeAll(async () => {
client = new AWS.S3({
endpoint: 'http://localhost:4566',
region: 'us-east-1',
forcePathStyle: true, // required for localstack
})
await createTestBucket()
})
await createTestBucket()
})
afterEach(async () => {
await clearTestBucket()
})
it('can upload', async () => {
const upload = await payload.create({
collection: 'media',
data: {},
filePath: path.resolve(__dirname, '../uploads/image.png'),
afterEach(async () => {
await clearTestBucket()
})
expect(upload.id).toBeTruthy()
it('can upload', async () => {
const upload = await payload.create({
collection: 'media',
data: {},
filePath: path.resolve(__dirname, '../uploads/image.png'),
})
await verifyUploads(upload.id)
expect(upload.id).toBeTruthy()
await verifyUploads(upload.id)
})
})
})
})
describe('Azure', () => {
it.todo('can upload')
})
describe('GCS', () => {
it.todo('can upload')
})
describe('R2', () => {
it.todo('can upload')
})
async function createTestBucket() {
const makeBucketRes = await client.send(new AWS.CreateBucketCommand({ Bucket: TEST_BUCKET }))
if (makeBucketRes.$metadata.httpStatusCode !== 200) {
throw new Error(`Failed to create bucket. ${makeBucketRes.$metadata.httpStatusCode}`)
}
}
async function clearTestBucket() {
const listedObjects = await client.send(
new AWS.ListObjectsV2Command({
Bucket: TEST_BUCKET,
}),
)
if (!listedObjects?.Contents?.length) return
const deleteParams = {
Bucket: TEST_BUCKET,
Delete: { Objects: [] },
}
listedObjects.Contents.forEach(({ Key }) => {
deleteParams.Delete.Objects.push({ Key })
describe('Azure', () => {
it.todo('can upload')
})
const deleteResult = await client.send(new AWS.DeleteObjectsCommand(deleteParams))
if (deleteResult.Errors?.length) {
throw new Error(JSON.stringify(deleteResult.Errors))
}
}
describe('GCS', () => {
it.todo('can upload')
})
async function verifyUploads(uploadId: number | string) {
try {
const uploadData = (await payload.findByID({
collection: 'media',
id: uploadId,
})) as unknown as { filename: string; sizes: Record<string, { filename: string }> }
describe('R2', () => {
it.todo('can upload')
})
const fileKeys = Object.keys(uploadData.sizes).map((key) => uploadData.sizes[key].filename)
fileKeys.push(uploadData.filename)
async function createTestBucket() {
const makeBucketRes = await client.send(new AWS.CreateBucketCommand({ Bucket: TEST_BUCKET }))
for (const key of fileKeys) {
const { $metadata } = await client.send(
new AWS.HeadObjectCommand({ Bucket: TEST_BUCKET, Key: key }),
)
// Verify each size was properly uploaded
expect($metadata.httpStatusCode).toBe(200)
if (makeBucketRes.$metadata.httpStatusCode !== 200) {
throw new Error(`Failed to create bucket. ${makeBucketRes.$metadata.httpStatusCode}`)
}
} catch (error: unknown) {
console.error('Error verifying uploads:', error)
throw error
}
}
async function clearTestBucket() {
const listedObjects = await client.send(
new AWS.ListObjectsV2Command({
Bucket: TEST_BUCKET,
}),
)
if (!listedObjects?.Contents?.length) return
const deleteParams = {
Bucket: TEST_BUCKET,
Delete: { Objects: [] },
}
listedObjects.Contents.forEach(({ Key }) => {
deleteParams.Delete.Objects.push({ Key })
})
const deleteResult = await client.send(new AWS.DeleteObjectsCommand(deleteParams))
if (deleteResult.Errors?.length) {
throw new Error(JSON.stringify(deleteResult.Errors))
}
}
async function verifyUploads(uploadId: number | string) {
try {
const uploadData = (await payload.findByID({
collection: 'media',
id: uploadId,
})) as unknown as { filename: string; sizes: Record<string, { filename: string }> }
const fileKeys = Object.keys(uploadData.sizes).map((key) => uploadData.sizes[key].filename)
fileKeys.push(uploadData.filename)
for (const key of fileKeys) {
const { $metadata } = await client.send(
new AWS.HeadObjectCommand({ Bucket: TEST_BUCKET, Key: key }),
)
// Verify each size was properly uploaded
expect($metadata.httpStatusCode).toBe(200)
}
} catch (error: unknown) {
console.error('Error verifying uploads:', error)
throw error
}
}
})

View File

@@ -20,9 +20,8 @@ let id: string
let payload: Payload
describe('SEO Plugin', () => {
beforeAll(async ({ browser }) => {
const { serverURL } = await initPayloadE2E({config, dirname: __dirname })
const { serverURL } = await initPayloadE2E({ config, dirname: __dirname })
url = new AdminUrlUtil(serverURL, 'pages')
const context = await browser.newContext()
@@ -38,7 +37,7 @@ describe('SEO Plugin', () => {
file,
})
const createdPage = await payload.create({
const createdPage = (await payload.create({
collection: 'pages',
data: {
slug: 'test-page',
@@ -50,7 +49,7 @@ describe('SEO Plugin', () => {
},
title: 'Test Page',
},
}) as unknown as Promise<PayloadPage>
})) as unknown as Promise<PayloadPage>
id = createdPage.id
})