Compare commits
12 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8c60c1c02 | ||
|
|
d44fb2db37 | ||
|
|
852f9fc1fd | ||
|
|
e2d803800d | ||
|
|
7fa68d17f5 | ||
|
|
9ec431a5bd | ||
|
|
cadf815ef6 | ||
|
|
638382e7fd | ||
|
|
08fdbcacc0 | ||
|
|
b27e42c484 | ||
|
|
32cc1a5761 | ||
|
|
38be69b7d3 |
@@ -5,13 +5,13 @@ export const recordLastLoggedInTenant: AfterLoginHook = async ({ req, user }) =>
|
||||
const relatedOrg = await req.payload
|
||||
.find({
|
||||
collection: 'tenants',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: {
|
||||
'domains.domain': {
|
||||
in: [req.headers.host],
|
||||
},
|
||||
},
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
})
|
||||
?.then((res) => res.docs?.[0])
|
||||
|
||||
@@ -24,7 +24,10 @@ export const recordLastLoggedInTenant: AfterLoginHook = async ({ req, user }) =>
|
||||
req,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
req.payload.logger.error(`Error recording last logged in tenant for user ${user.id}: ${err}`)
|
||||
req.payload.logger.error({
|
||||
err,
|
||||
msg: `Error recording last logged in tenant for user ${user.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
return user
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -23,7 +23,7 @@ const connectWithReconnect = async function ({
|
||||
} else {
|
||||
try {
|
||||
result = await adapter.pool.connect()
|
||||
} catch (err) {
|
||||
} catch (ignore) {
|
||||
setTimeout(() => {
|
||||
payload.logger.info('Reconnecting to postgres')
|
||||
void connectWithReconnect({ adapter, payload, reconnect: true })
|
||||
@@ -38,7 +38,7 @@ const connectWithReconnect = async function ({
|
||||
if (err.code === 'ECONNRESET') {
|
||||
void connectWithReconnect({ adapter, payload, reconnect: true })
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (ignore) {
|
||||
// swallow error
|
||||
}
|
||||
})
|
||||
@@ -76,7 +76,7 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
|
||||
this.payload.logger.error({ err, msg: `Error: cannot connect to Postgres: ${err.message}` })
|
||||
if (typeof this.rejectInitializing === 'function') {
|
||||
this.rejectInitializing()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-sqlite",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The officially supported SQLite database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -37,7 +37,7 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot connect to SQLite. Details: ${err.message}`, err)
|
||||
this.payload.logger.error({ err, msg: `Error: cannot connect to SQLite: ${err.message}` })
|
||||
if (typeof this.rejectInitializing === 'function') {
|
||||
this.rejectInitializing()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-vercel-postgres",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Vercel Postgres adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -39,7 +39,7 @@ export const connect: Connect = async function connect(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot connect to Postgres. Details: ${err.message}`, err)
|
||||
this.payload.logger.error({ err, msg: `Error: cannot connect to Postgres: ${err.message}` })
|
||||
if (typeof this.rejectInitializing === 'function') {
|
||||
this.rejectInitializing()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/drizzle",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "A library of shared functions used by different payload database adapters",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -37,6 +37,7 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
|
||||
return done
|
||||
}
|
||||
reject = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
||||
rej()
|
||||
return done
|
||||
}
|
||||
@@ -57,7 +58,7 @@ export const beginTransaction: BeginTransaction = async function beginTransactio
|
||||
resolve,
|
||||
}
|
||||
} catch (err) {
|
||||
this.payload.logger.error(`Error: cannot begin transaction: ${err.message}`, err)
|
||||
this.payload.logger.error({ err, msg: `Error: cannot begin transaction: ${err.message}` })
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The official React SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-vue",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The official Vue SDK for Payload Live Preview",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
useListQuery,
|
||||
useModal,
|
||||
useRouteCache,
|
||||
useSearchParams,
|
||||
useStepNav,
|
||||
useTranslation,
|
||||
useWindowInfo,
|
||||
@@ -54,8 +53,7 @@ export const DefaultListView: React.FC = () => {
|
||||
newDocumentURL,
|
||||
} = useListInfo()
|
||||
|
||||
const { data, defaultLimit, handlePageChange, handlePerPageChange } = useListQuery()
|
||||
const { searchParams } = useSearchParams()
|
||||
const { data, defaultLimit, handlePageChange, handlePerPageChange, params } = useListQuery()
|
||||
const { openModal } = useModal()
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
const { setCollectionSlug, setOnSuccess } = useBulkUpload()
|
||||
@@ -226,9 +224,7 @@ export const DefaultListView: React.FC = () => {
|
||||
</div>
|
||||
<PerPage
|
||||
handleChange={(limit) => void handlePerPageChange(limit)}
|
||||
limit={
|
||||
isNumber(searchParams?.limit) ? Number(searchParams.limit) : defaultLimit
|
||||
}
|
||||
limit={isNumber(params?.limit) ? Number(params.limit) : defaultLimit}
|
||||
limits={collectionConfig?.admin?.pagination?.limits}
|
||||
resetPage={data.totalDocs <= data.pagingCounter}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -2,8 +2,6 @@ import type { TypedUser } from '../../index.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
import type { Permissions } from '../types.js'
|
||||
|
||||
import { commitTransaction } from '../../utilities/commitTransaction.js'
|
||||
import { initTransaction } from '../../utilities/initTransaction.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import { executeAuthStrategies } from '../executeAuthStrategies.js'
|
||||
import { getAccessResults } from '../getAccessResults.js'
|
||||
@@ -25,8 +23,6 @@ export const auth = async (args: Required<AuthArgs>): Promise<AuthResult> => {
|
||||
const { payload } = req
|
||||
|
||||
try {
|
||||
const shouldCommit = await initTransaction(req)
|
||||
|
||||
const { responseHeaders, user } = await executeAuthStrategies({
|
||||
headers,
|
||||
payload,
|
||||
@@ -39,10 +35,6 @@ export const auth = async (args: Required<AuthArgs>): Promise<AuthResult> => {
|
||||
req,
|
||||
})
|
||||
|
||||
if (shouldCommit) {
|
||||
await commitTransaction(req)
|
||||
}
|
||||
|
||||
return {
|
||||
permissions,
|
||||
responseHeaders,
|
||||
|
||||
@@ -12,8 +12,6 @@ import type { User } from '../types.js'
|
||||
import { buildAfterOperation } from '../../collections/operations/utils.js'
|
||||
import { AuthenticationError, LockedAuth, ValidationError } from '../../errors/index.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { commitTransaction } from '../../utilities/commitTransaction.js'
|
||||
import { initTransaction } from '../../utilities/initTransaction.js'
|
||||
import { killTransaction } from '../../utilities/killTransaction.js'
|
||||
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
|
||||
import { getFieldsToSign } from '../getFieldsToSign.js'
|
||||
@@ -43,8 +41,6 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
|
||||
let args = incomingArgs
|
||||
|
||||
try {
|
||||
const shouldCommit = await initTransaction(args.req)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
@@ -202,10 +198,6 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldCommit) {
|
||||
await commitTransaction(req)
|
||||
}
|
||||
|
||||
throw new AuthenticationError(req.t)
|
||||
}
|
||||
|
||||
@@ -334,10 +326,6 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (shouldCommit) {
|
||||
await commitTransaction(req)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error: unknown) {
|
||||
await killTransaction(args.req)
|
||||
|
||||
@@ -14,6 +14,7 @@ import baseVersionFields from '../../versions/baseFields.js'
|
||||
import { versionDefaults } from '../../versions/defaults.js'
|
||||
import { authDefaults, defaults, loginWithUsernameDefaults } from './defaults.js'
|
||||
import { sanitizeAuthFields, sanitizeUploadFields } from './reservedFieldNames.js'
|
||||
import { validateUseAsTitle } from './useAsTitle.js'
|
||||
|
||||
export const sanitizeCollection = async (
|
||||
config: Config,
|
||||
@@ -44,6 +45,8 @@ export const sanitizeCollection = async (
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
validateUseAsTitle(sanitized)
|
||||
|
||||
if (sanitized.timestamps !== false) {
|
||||
// add default timestamps fields only as needed
|
||||
let hasUpdatedAt = null
|
||||
|
||||
204
packages/payload/src/collections/config/useAsTitle.spec.ts
Normal file
204
packages/payload/src/collections/config/useAsTitle.spec.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { Config } from '../../config/types.js'
|
||||
import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
|
||||
import { sanitizeCollection } from './sanitize.js'
|
||||
|
||||
describe('sanitize - collections -', () => {
|
||||
const config = {
|
||||
collections: [],
|
||||
globals: [],
|
||||
} as Partial<Config>
|
||||
|
||||
describe('validate useAsTitle -', () => {
|
||||
const defaultCollection: CollectionConfig = {
|
||||
slug: 'collection-with-defaults',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
it('should throw on invalid field', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'invalidField',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).rejects.toThrow(InvalidConfiguration)
|
||||
})
|
||||
|
||||
it('should not throw on valid field', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on valid field inside tabs', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: 'General',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on valid field inside collapsibles', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'Collapsible',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should throw on nested useAsTitle', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'content.title',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).rejects.toThrow(InvalidConfiguration)
|
||||
})
|
||||
|
||||
it('should not throw on default field: id', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'id',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not throw on default field: email if auth is enabled', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).not.toThrow()
|
||||
})
|
||||
it('should throw on default field: email if auth is not enabled', async () => {
|
||||
const collectionConfig: CollectionConfig = {
|
||||
...defaultCollection,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
}
|
||||
await expect(async () => {
|
||||
await sanitizeCollection(
|
||||
// @ts-expect-error
|
||||
{
|
||||
...config,
|
||||
collections: [collectionConfig],
|
||||
},
|
||||
collectionConfig,
|
||||
)
|
||||
}).rejects.toThrow(InvalidConfiguration)
|
||||
})
|
||||
})
|
||||
})
|
||||
43
packages/payload/src/collections/config/useAsTitle.ts
Normal file
43
packages/payload/src/collections/config/useAsTitle.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { CollectionConfig } from '../../index.js'
|
||||
|
||||
import { InvalidConfiguration } from '../../errors/InvalidConfiguration.js'
|
||||
import { fieldAffectsData } from '../../fields/config/types.js'
|
||||
import flattenFields from '../../utilities/flattenTopLevelFields.js'
|
||||
|
||||
/**
|
||||
* Validate useAsTitle for collections.
|
||||
*/
|
||||
export const validateUseAsTitle = (config: CollectionConfig) => {
|
||||
if (config.admin.useAsTitle.includes('.')) {
|
||||
throw new InvalidConfiguration(
|
||||
`"useAsTitle" cannot be a nested field. Please specify a top-level field in the collection "${config.slug}"`,
|
||||
)
|
||||
}
|
||||
|
||||
if (config?.admin && config.admin?.useAsTitle && config.admin.useAsTitle !== 'id') {
|
||||
const fields = flattenFields(config.fields)
|
||||
const useAsTitleField = fields.find((field) => {
|
||||
if (fieldAffectsData(field) && config.admin) {
|
||||
return field.name === config.admin.useAsTitle
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// If auth is enabled then we don't need to
|
||||
if (config.auth) {
|
||||
if (config.admin.useAsTitle !== 'email') {
|
||||
if (!useAsTitleField) {
|
||||
throw new InvalidConfiguration(
|
||||
`The field "${config.admin.useAsTitle}" specified in "admin.useAsTitle" does not exist in the collection "${config.slug}"`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!useAsTitleField) {
|
||||
throw new InvalidConfiguration(
|
||||
`The field "${config.admin.useAsTitle}" specified in "admin.useAsTitle" does not exist in the collection "${config.slug}"`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,8 +136,8 @@ const batchAndLoadDocs =
|
||||
depth,
|
||||
docID: doc.id,
|
||||
draft,
|
||||
fallbackLocale: req.fallbackLocale,
|
||||
locale: req.locale,
|
||||
fallbackLocale,
|
||||
locale,
|
||||
overrideAccess,
|
||||
showHiddenFields,
|
||||
transactionID: req.transactionID,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import type { Document, Where } from '../../types/index.js'
|
||||
import type { PreferenceRequest } from '../types.js'
|
||||
|
||||
import defaultAccess from '../../auth/defaultAccess.js'
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { NotFound } from '../../errors/NotFound.js'
|
||||
import { UnauthorizedError } from '../../errors/UnathorizedError.js'
|
||||
|
||||
async function deleteOperation(args: PreferenceRequest): Promise<Document> {
|
||||
export async function deleteOperation(args: PreferenceRequest): Promise<Document> {
|
||||
const {
|
||||
key,
|
||||
overrideAccess,
|
||||
req: { payload },
|
||||
req,
|
||||
user,
|
||||
@@ -19,10 +16,6 @@ async function deleteOperation(args: PreferenceRequest): Promise<Document> {
|
||||
throw new UnauthorizedError(req.t)
|
||||
}
|
||||
|
||||
if (!overrideAccess) {
|
||||
await executeAccess({ req }, defaultAccess)
|
||||
}
|
||||
|
||||
const where: Where = {
|
||||
and: [
|
||||
{ key: { equals: key } },
|
||||
@@ -42,5 +35,3 @@ async function deleteOperation(args: PreferenceRequest): Promise<Document> {
|
||||
}
|
||||
throw new NotFound(req.t)
|
||||
}
|
||||
|
||||
export default deleteOperation
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TypedCollection } from '../../index.js'
|
||||
import type { Where } from '../../types/index.js'
|
||||
import type { PreferenceRequest } from '../types.js'
|
||||
|
||||
async function findOne(args: PreferenceRequest): Promise<TypedCollection['_preference']> {
|
||||
export async function findOne(args: PreferenceRequest): Promise<TypedCollection['_preference']> {
|
||||
const {
|
||||
key,
|
||||
req: { payload },
|
||||
@@ -22,11 +22,14 @@ async function findOne(args: PreferenceRequest): Promise<TypedCollection['_prefe
|
||||
],
|
||||
}
|
||||
|
||||
return await payload.db.findOne({
|
||||
const { docs } = await payload.db.find({
|
||||
collection: 'payload-preferences',
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
req,
|
||||
sort: '-updatedAt',
|
||||
where,
|
||||
})
|
||||
}
|
||||
|
||||
export default findOne
|
||||
return docs?.[0] || null
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { Where } from '../../types/index.js'
|
||||
import type { PreferenceUpdateRequest } from '../types.js'
|
||||
|
||||
import defaultAccess from '../../auth/defaultAccess.js'
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { UnauthorizedError } from '../../errors/UnathorizedError.js'
|
||||
|
||||
async function update(args: PreferenceUpdateRequest) {
|
||||
export async function update(args: PreferenceUpdateRequest) {
|
||||
const {
|
||||
key,
|
||||
overrideAccess,
|
||||
req: { payload },
|
||||
req,
|
||||
user,
|
||||
@@ -20,10 +18,12 @@ async function update(args: PreferenceUpdateRequest) {
|
||||
|
||||
const collection = 'payload-preferences'
|
||||
|
||||
const filter = {
|
||||
key: { equals: key },
|
||||
'user.relationTo': { equals: user.collection },
|
||||
'user.value': { equals: user.id },
|
||||
const where: Where = {
|
||||
and: [
|
||||
{ key: { equals: key } },
|
||||
{ 'user.value': { equals: user.id } },
|
||||
{ 'user.relationTo': { equals: user.collection } },
|
||||
],
|
||||
}
|
||||
|
||||
const preference = {
|
||||
@@ -35,27 +35,23 @@ async function update(args: PreferenceUpdateRequest) {
|
||||
value,
|
||||
}
|
||||
|
||||
if (!overrideAccess) {
|
||||
await executeAccess({ req }, defaultAccess)
|
||||
}
|
||||
let result
|
||||
|
||||
try {
|
||||
// try/catch because we attempt to update without first reading to check if it exists first to save on db calls
|
||||
await payload.db.updateOne({
|
||||
result = await payload.db.updateOne({
|
||||
collection,
|
||||
data: preference,
|
||||
req,
|
||||
where: filter,
|
||||
where,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
await payload.db.create({
|
||||
result = await payload.db.create({
|
||||
collection,
|
||||
data: preference,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
return preference
|
||||
return result
|
||||
}
|
||||
|
||||
export default update
|
||||
|
||||
@@ -3,7 +3,7 @@ import httpStatus from 'http-status'
|
||||
import type { PayloadHandler } from '../../config/types.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
|
||||
import deleteOperation from '../operations/delete.js'
|
||||
import { deleteOperation } from '../operations/delete.js'
|
||||
|
||||
export const deleteHandler: PayloadHandler = async (incomingReq): Promise<Response> => {
|
||||
// We cannot import the addDataAndFileToRequest utility here from the 'next' package because of dependency issues
|
||||
|
||||
@@ -3,7 +3,7 @@ import httpStatus from 'http-status'
|
||||
import type { PayloadHandler } from '../../config/types.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
|
||||
import findOne from '../operations/findOne.js'
|
||||
import { findOne } from '../operations/findOne.js'
|
||||
|
||||
export const findByIDHandler: PayloadHandler = async (incomingReq): Promise<Response> => {
|
||||
// We cannot import the addDataAndFileToRequest utility here from the 'next' package because of dependency issues
|
||||
|
||||
@@ -3,7 +3,7 @@ import httpStatus from 'http-status'
|
||||
import type { PayloadHandler } from '../../config/types.js'
|
||||
import type { PayloadRequest } from '../../types/index.js'
|
||||
|
||||
import update from '../operations/update.js'
|
||||
import { update } from '../operations/update.js'
|
||||
|
||||
export const updateHandler: PayloadHandler = async (incomingReq) => {
|
||||
// We cannot import the addDataAndFileToRequest utility here from the 'next' package because of dependency issues
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-relationship-object-ids",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-s3",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Payload storage adapter for Amazon S3",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-uploadthing",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Payload storage adapter for uploadthing",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-vercel-blob",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"description": "Payload storage adapter for Vercel Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/translations",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/ui",
|
||||
"version": "3.0.0-beta.100",
|
||||
"version": "3.0.0-beta.101",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -82,12 +82,12 @@
|
||||
background-color: var(--theme-elevation-100);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
.thumbnail {
|
||||
width: base(1.2);
|
||||
height: base(1.2);
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
border-radius: var(--style-radius-m);
|
||||
border-radius: var(--style-radius-s);
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { useWindowInfo } from '@faceless-ui/window-info'
|
||||
import { isImage } from 'payload/shared'
|
||||
import React from 'react'
|
||||
import AnimateHeightImport from 'react-animate-height'
|
||||
|
||||
@@ -13,6 +14,7 @@ import { Drawer } from '../../Drawer/index.js'
|
||||
import { ErrorPill } from '../../ErrorPill/index.js'
|
||||
import { Pill } from '../../Pill/index.js'
|
||||
import { ShimmerEffect } from '../../ShimmerEffect/index.js'
|
||||
import { Thumbnail } from '../../Thumbnail/index.js'
|
||||
import { Actions } from '../ActionsBar/index.js'
|
||||
import { AddFilesView } from '../AddFilesView/index.js'
|
||||
import { useFormsManager } from '../FormsManager/index.js'
|
||||
@@ -144,7 +146,12 @@ export function FileSidebar() {
|
||||
onClick={() => setActiveIndex(index)}
|
||||
type="button"
|
||||
>
|
||||
<img alt={currentFile.name} src={URL.createObjectURL(currentFile)} />
|
||||
<Thumbnail
|
||||
className={`${baseClass}__thumbnail`}
|
||||
fileSrc={
|
||||
isImage(currentFile.type) ? URL.createObjectURL(currentFile) : undefined
|
||||
}
|
||||
/>
|
||||
<div className={`${baseClass}__fileDetails`}>
|
||||
<p className={`${baseClass}__fileName`} title={currentFile.name}>
|
||||
{currentFile.name}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { ChevronIcon } from '../../icons/Chevron/index.js'
|
||||
import { SearchIcon } from '../../icons/Search/index.js'
|
||||
import { useListInfo } from '../../providers/ListInfo/index.js'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { ColumnSelector } from '../ColumnSelector/index.js'
|
||||
import { DeleteMany } from '../DeleteMany/index.js'
|
||||
@@ -48,17 +47,13 @@ export type ListControlsProps = {
|
||||
export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
const { collectionConfig, enableColumns = true, enableSort = false, fields } = props
|
||||
|
||||
const { handleSearchChange } = useListQuery()
|
||||
const { handleSearchChange, params } = useListQuery()
|
||||
const { beforeActions, collectionSlug, disableBulkDelete, disableBulkEdit } = useListInfo()
|
||||
const { searchParams } = useSearchParams()
|
||||
const titleField = useUseTitleField(collectionConfig, fields)
|
||||
const { i18n, t } = useTranslation()
|
||||
const {
|
||||
breakpoints: { s: smallBreak },
|
||||
} = useWindowInfo()
|
||||
const [search, setSearch] = useState(
|
||||
typeof searchParams?.search === 'string' ? searchParams?.search : '',
|
||||
)
|
||||
|
||||
const searchLabel =
|
||||
(titleField &&
|
||||
@@ -81,21 +76,21 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
t('general:searchBy', { label: getTranslation(searchLabel, i18n) }),
|
||||
)
|
||||
|
||||
const hasWhereParam = useRef(Boolean(searchParams?.where))
|
||||
const hasWhereParam = useRef(Boolean(params?.where))
|
||||
|
||||
const shouldInitializeWhereOpened = validateWhereQuery(searchParams?.where)
|
||||
const shouldInitializeWhereOpened = validateWhereQuery(params?.where)
|
||||
const [visibleDrawer, setVisibleDrawer] = useState<'columns' | 'sort' | 'where'>(
|
||||
shouldInitializeWhereOpened ? 'where' : undefined,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasWhereParam.current && !searchParams?.where) {
|
||||
if (hasWhereParam.current && !params?.where) {
|
||||
setVisibleDrawer(undefined)
|
||||
hasWhereParam.current = false
|
||||
} else if (searchParams?.where) {
|
||||
} else if (params?.where) {
|
||||
hasWhereParam.current = true
|
||||
}
|
||||
}, [setVisibleDrawer, searchParams?.where])
|
||||
}, [setVisibleDrawer, params?.where])
|
||||
|
||||
useEffect(() => {
|
||||
if (listSearchableFields?.length > 0) {
|
||||
@@ -134,11 +129,10 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
handleChange={(search) => {
|
||||
return void handleSearchChange(search)
|
||||
}}
|
||||
initialParams={searchParams}
|
||||
// @ts-expect-error @todo: fix types
|
||||
initialParams={params}
|
||||
key={collectionSlug}
|
||||
label={searchLabelTranslated.current}
|
||||
setValue={setSearch}
|
||||
value={search}
|
||||
/>
|
||||
<div className={`${baseClass}__buttons`}>
|
||||
<div className={`${baseClass}__buttons-wrap`}>
|
||||
@@ -216,7 +210,7 @@ export const ListControls: React.FC<ListControlsProps> = (props) => {
|
||||
collectionPluralLabel={collectionConfig?.labels?.plural}
|
||||
collectionSlug={collectionConfig.slug}
|
||||
fields={fields}
|
||||
key={String(hasWhereParam.current && !searchParams?.where)}
|
||||
key={String(hasWhereParam.current && !params?.where)}
|
||||
/>
|
||||
</AnimateHeight>
|
||||
{enableSort && (
|
||||
|
||||
@@ -3,13 +3,14 @@ import type { ClientCollectionConfig, Where } from 'payload'
|
||||
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||
|
||||
import type { ListDrawerProps } from './types.js'
|
||||
|
||||
import { SelectMany } from '../../elements/SelectMany/index.js'
|
||||
import { FieldLabel } from '../../fields/FieldLabel/index.js'
|
||||
import { usePayloadAPI } from '../../hooks/usePayloadAPI.js'
|
||||
import { useThrottledEffect } from '../../hooks/useThrottledEffect.js'
|
||||
import { XIcon } from '../../icons/X/index.js'
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
@@ -54,13 +55,25 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const { permissions } = useAuth()
|
||||
const { setPreference } = usePreferences()
|
||||
const { getPreference, setPreference } = usePreferences()
|
||||
const { closeModal, isModalOpen } = useModal()
|
||||
const [limit, setLimit] = useState<number>()
|
||||
// Track the page limit so we can reset the page number when it changes
|
||||
const previousLimit = useRef<number>(limit || null)
|
||||
const [sort, setSort] = useState<string>(null)
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const [where, setWhere] = useState<Where>(null)
|
||||
const [search, setSearch] = useState<string>('')
|
||||
const [showLoadingOverlay, setShowLoadingOverlay] = useState<boolean>(true)
|
||||
const hasInitialised = useRef(false)
|
||||
|
||||
const params = {
|
||||
limit,
|
||||
page,
|
||||
search,
|
||||
sort,
|
||||
where,
|
||||
}
|
||||
|
||||
const {
|
||||
config: {
|
||||
@@ -94,12 +107,6 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
: undefined,
|
||||
)
|
||||
|
||||
// const [fields, setFields] = useState<Field[]>(() => formatFields(selectedCollectionConfig))
|
||||
|
||||
useEffect(() => {
|
||||
// setFields(formatFields(selectedCollectionConfig))
|
||||
}, [selectedCollectionConfig])
|
||||
|
||||
// allow external control of selected collection, same as the initial state logic above
|
||||
useEffect(() => {
|
||||
if (selectedCollection) {
|
||||
@@ -111,7 +118,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}
|
||||
}, [selectedCollection, enabledCollectionConfigs, onSelect, t])
|
||||
|
||||
const preferenceKey = `${selectedCollectionConfig.slug}-list`
|
||||
const preferencesKey = `${selectedCollectionConfig.slug}-list`
|
||||
|
||||
// this is the 'create new' drawer
|
||||
const [DocumentDrawer, DocumentDrawerToggler, { drawerSlug: documentDrawerSlug }] =
|
||||
@@ -147,6 +154,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
admin: { listSearchableFields, useAsTitle } = {},
|
||||
versions,
|
||||
} = selectedCollectionConfig
|
||||
|
||||
const params: {
|
||||
cacheBust?: number
|
||||
depth?: number
|
||||
@@ -194,6 +202,17 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
if (cacheBust) {
|
||||
params.cacheBust = cacheBust
|
||||
}
|
||||
if (limit) {
|
||||
params.limit = limit
|
||||
|
||||
if (limit !== previousLimit.current) {
|
||||
previousLimit.current = limit
|
||||
|
||||
// Reset page if limit changes
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setPage(1)
|
||||
}
|
||||
}
|
||||
if (copyOfWhere) {
|
||||
params.where = copyOfWhere
|
||||
}
|
||||
@@ -202,7 +221,18 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
}
|
||||
|
||||
setParams(params)
|
||||
}, [page, sort, where, search, cacheBust, filterOptions, selectedCollectionConfig, t, setParams])
|
||||
}, [
|
||||
page,
|
||||
sort,
|
||||
where,
|
||||
search,
|
||||
limit,
|
||||
cacheBust,
|
||||
filterOptions,
|
||||
selectedCollectionConfig,
|
||||
t,
|
||||
setParams,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const newPreferences = {
|
||||
@@ -210,8 +240,49 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
sort,
|
||||
}
|
||||
|
||||
void setPreference(preferenceKey, newPreferences, true)
|
||||
}, [sort, limit, setPreference, preferenceKey])
|
||||
if (limit || sort) {
|
||||
void setPreference(preferencesKey, newPreferences, true)
|
||||
}
|
||||
}, [sort, limit, setPreference, preferencesKey])
|
||||
|
||||
// Get existing preferences if they exist
|
||||
useEffect(() => {
|
||||
if (preferencesKey && !limit) {
|
||||
const getInitialPref = async () => {
|
||||
const existingPreferences = await getPreference<{ limit?: number }>(preferencesKey)
|
||||
|
||||
if (existingPreferences?.limit) {
|
||||
setLimit(existingPreferences?.limit)
|
||||
}
|
||||
}
|
||||
void getInitialPref()
|
||||
}
|
||||
}, [getPreference, limit, preferencesKey])
|
||||
|
||||
useThrottledEffect(
|
||||
() => {
|
||||
if (isLoadingList) {
|
||||
setShowLoadingOverlay(true)
|
||||
}
|
||||
},
|
||||
1750,
|
||||
[isLoadingList, setShowLoadingOverlay],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
hasInitialised.current = true
|
||||
} else {
|
||||
hasInitialised.current = false
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingList && showLoadingOverlay) {
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setShowLoadingOverlay(false)
|
||||
}
|
||||
}, [isLoadingList, showLoadingOverlay])
|
||||
|
||||
const onCreateNew = useCallback(
|
||||
({ doc }) => {
|
||||
@@ -232,108 +303,111 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = ({
|
||||
return null
|
||||
}
|
||||
|
||||
if (isLoadingList) {
|
||||
return <LoadingOverlay />
|
||||
}
|
||||
|
||||
return (
|
||||
<ListInfoProvider
|
||||
beforeActions={
|
||||
enableRowSelections ? [<SelectMany key="select-many" onClick={onBulkSelect} />] : undefined
|
||||
}
|
||||
collectionConfig={selectedCollectionConfig}
|
||||
collectionSlug={selectedCollectionConfig.slug}
|
||||
disableBulkDelete
|
||||
disableBulkEdit
|
||||
hasCreatePermission={hasCreatePermission}
|
||||
Header={
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header-content`}>
|
||||
<h2 className={`${baseClass}__header-text`}>
|
||||
{!customHeader
|
||||
? getTranslation(selectedCollectionConfig?.labels?.plural, i18n)
|
||||
: customHeader}
|
||||
</h2>
|
||||
{hasCreatePermission && (
|
||||
<DocumentDrawerToggler className={`${baseClass}__create-new-button`}>
|
||||
<Pill>{t('general:createNew')}</Pill>
|
||||
</DocumentDrawerToggler>
|
||||
)}
|
||||
<>
|
||||
{showLoadingOverlay && <LoadingOverlay />}
|
||||
<ListInfoProvider
|
||||
beforeActions={
|
||||
enableRowSelections
|
||||
? [<SelectMany key="select-many" onClick={onBulkSelect} />]
|
||||
: undefined
|
||||
}
|
||||
collectionConfig={selectedCollectionConfig}
|
||||
collectionSlug={selectedCollectionConfig.slug}
|
||||
disableBulkDelete
|
||||
disableBulkEdit
|
||||
hasCreatePermission={hasCreatePermission}
|
||||
Header={
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<div className={`${baseClass}__header-content`}>
|
||||
<h2 className={`${baseClass}__header-text`}>
|
||||
{!customHeader
|
||||
? getTranslation(selectedCollectionConfig?.labels?.plural, i18n)
|
||||
: customHeader}
|
||||
</h2>
|
||||
{hasCreatePermission && (
|
||||
<DocumentDrawerToggler className={`${baseClass}__create-new-button`}>
|
||||
<Pill>{t('general:createNew')}</Pill>
|
||||
</DocumentDrawerToggler>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
aria-label={t('general:close')}
|
||||
className={`${baseClass}__header-close`}
|
||||
onClick={() => {
|
||||
closeModal(drawerSlug)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
aria-label={t('general:close')}
|
||||
className={`${baseClass}__header-close`}
|
||||
onClick={() => {
|
||||
closeModal(drawerSlug)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
{(selectedCollectionConfig?.admin?.description ||
|
||||
selectedCollectionConfig?.admin?.components?.Description) && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription
|
||||
Description={selectedCollectionConfig.admin?.components?.Description}
|
||||
description={selectedCollectionConfig.admin?.description}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{moreThanOneAvailableCollection && (
|
||||
<div className={`${baseClass}__select-collection-wrap`}>
|
||||
<FieldLabel field={null} label={t('upload:selectCollectionToBrowse')} />
|
||||
<ReactSelect
|
||||
className={`${baseClass}__select-collection`}
|
||||
onChange={setSelectedOption} // this is only changing the options which is not rerunning my effect
|
||||
options={enabledCollectionConfigs.map((coll) => ({
|
||||
label: getTranslation(coll.labels.singular, i18n),
|
||||
value: coll.slug,
|
||||
}))}
|
||||
value={selectedOption}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
}
|
||||
newDocumentURL={null}
|
||||
>
|
||||
<ListQueryProvider
|
||||
data={data}
|
||||
defaultLimit={limit || selectedCollectionConfig?.admin?.pagination?.defaultLimit}
|
||||
defaultSort={sort}
|
||||
handlePageChange={setPage}
|
||||
handlePerPageChange={setLimit}
|
||||
handleSearchChange={setSearch}
|
||||
handleSortChange={setSort}
|
||||
handleWhereChange={setWhere}
|
||||
modifySearchParams={false}
|
||||
preferenceKey={preferenceKey}
|
||||
{(selectedCollectionConfig?.admin?.description ||
|
||||
selectedCollectionConfig?.admin?.components?.Description) && (
|
||||
<div className={`${baseClass}__sub-header`}>
|
||||
<ViewDescription
|
||||
Description={selectedCollectionConfig.admin?.components?.Description}
|
||||
description={selectedCollectionConfig.admin?.description}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{moreThanOneAvailableCollection && (
|
||||
<div className={`${baseClass}__select-collection-wrap`}>
|
||||
<FieldLabel field={null} label={t('upload:selectCollectionToBrowse')} />
|
||||
<ReactSelect
|
||||
className={`${baseClass}__select-collection`}
|
||||
onChange={setSelectedOption} // this is only changing the options which is not rerunning my effect
|
||||
options={enabledCollectionConfigs.map((coll) => ({
|
||||
label: getTranslation(coll.labels.singular, i18n),
|
||||
value: coll.slug,
|
||||
}))}
|
||||
value={selectedOption}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
}
|
||||
newDocumentURL={null}
|
||||
>
|
||||
<TableColumnsProvider
|
||||
cellProps={[
|
||||
{
|
||||
className: `${baseClass}__first-cell`,
|
||||
link: false,
|
||||
onClick: ({ collectionSlug: rowColl, rowData }) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
collectionSlug: rowColl,
|
||||
docID: rowData.id as string,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
collectionSlug={selectedCollectionConfig.slug}
|
||||
enableRowSelections={enableRowSelections}
|
||||
preferenceKey={preferenceKey}
|
||||
<ListQueryProvider
|
||||
data={data}
|
||||
defaultLimit={limit || selectedCollectionConfig?.admin?.pagination?.defaultLimit}
|
||||
defaultSort={sort}
|
||||
handlePageChange={setPage}
|
||||
handlePerPageChange={setLimit}
|
||||
handleSearchChange={setSearch}
|
||||
handleSortChange={setSort}
|
||||
handleWhereChange={setWhere}
|
||||
modifySearchParams={false}
|
||||
// @ts-expect-error todo: fix types
|
||||
params={params}
|
||||
preferenceKey={preferencesKey}
|
||||
>
|
||||
<RenderComponent mappedComponent={List} />
|
||||
<DocumentDrawer onSave={onCreateNew} />
|
||||
</TableColumnsProvider>
|
||||
</ListQueryProvider>
|
||||
</ListInfoProvider>
|
||||
<TableColumnsProvider
|
||||
cellProps={[
|
||||
{
|
||||
className: `${baseClass}__first-cell`,
|
||||
link: false,
|
||||
onClick: ({ collectionSlug: rowColl, rowData }) => {
|
||||
if (typeof onSelect === 'function') {
|
||||
onSelect({
|
||||
collectionSlug: rowColl,
|
||||
docID: rowData.id as string,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
collectionSlug={selectedCollectionConfig.slug}
|
||||
enableRowSelections={enableRowSelections}
|
||||
preferenceKey={preferencesKey}
|
||||
>
|
||||
<RenderComponent mappedComponent={List} />
|
||||
<DocumentDrawer onSave={onCreateNew} />
|
||||
</TableColumnsProvider>
|
||||
</ListQueryProvider>
|
||||
</ListInfoProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export type SearchFilterProps = {
|
||||
fieldName?: string
|
||||
@@ -12,32 +12,53 @@ export type SearchFilterProps = {
|
||||
|
||||
import type { ParsedQs } from 'qs-esm'
|
||||
|
||||
import { usePathname } from 'next/navigation.js'
|
||||
|
||||
import { useDebounce } from '../../hooks/useDebounce.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'search-filter'
|
||||
|
||||
export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
|
||||
const { handleChange, initialParams, label, setValue, value } = props
|
||||
|
||||
const previousSearch = useRef(
|
||||
typeof initialParams?.search === 'string' ? initialParams?.search : '',
|
||||
const { handleChange, initialParams, label } = props
|
||||
const pathname = usePathname()
|
||||
const [search, setSearch] = useState(
|
||||
typeof initialParams?.search === 'string' ? initialParams?.search : undefined,
|
||||
)
|
||||
|
||||
const debouncedSearch = useDebounce(value, 300)
|
||||
/**
|
||||
* Tracks whether the state should be updated based on the search value.
|
||||
* If the value is updated from the URL, we don't want to update the state as it causes additional renders.
|
||||
*/
|
||||
const shouldUpdateState = useRef(true)
|
||||
|
||||
/**
|
||||
* Tracks the previous search value to compare with the current debounced search value.
|
||||
*/
|
||||
const previousSearch = useRef(
|
||||
typeof initialParams?.search === 'string' ? initialParams?.search : undefined,
|
||||
)
|
||||
|
||||
const debouncedSearch = useDebounce(search, 300)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch !== previousSearch.current) {
|
||||
if (initialParams?.search !== previousSearch.current) {
|
||||
shouldUpdateState.current = false
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setSearch(initialParams?.search as string)
|
||||
previousSearch.current = initialParams?.search as string
|
||||
}
|
||||
}, [initialParams?.search, pathname])
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch !== previousSearch.current && shouldUpdateState.current) {
|
||||
if (handleChange) {
|
||||
handleChange(debouncedSearch)
|
||||
}
|
||||
|
||||
previousSearch.current = debouncedSearch
|
||||
}
|
||||
}, [debouncedSearch, previousSearch, handleChange])
|
||||
|
||||
// Cleans up the search input when the component is unmounted
|
||||
useEffect(() => () => setValue(''), [])
|
||||
}, [debouncedSearch, handleChange])
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -45,10 +66,13 @@ export const SearchFilter: React.FC<SearchFilterProps> = (props) => {
|
||||
aria-label={label}
|
||||
className={`${baseClass}__input`}
|
||||
id="search-filter-input"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onChange={(e) => {
|
||||
shouldUpdateState.current = true
|
||||
setSearch(e.target.value)
|
||||
}}
|
||||
placeholder={label}
|
||||
type="text"
|
||||
value={value || ''}
|
||||
value={search || ''}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import React from 'react'
|
||||
|
||||
import { ChevronIcon } from '../../icons/Chevron/index.js'
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import './index.scss'
|
||||
|
||||
@@ -20,11 +19,10 @@ const baseClass = 'sort-column'
|
||||
|
||||
export const SortColumn: React.FC<SortColumnProps> = (props) => {
|
||||
const { name, disable = false, Label, label } = props
|
||||
const { searchParams } = useSearchParams()
|
||||
const { handleSortChange } = useListQuery()
|
||||
const { handleSortChange, params } = useListQuery()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { sort } = searchParams
|
||||
const { sort } = params
|
||||
|
||||
const desc = `-${name}`
|
||||
const asc = name
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
|
||||
&__dropzoneContent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: base(0.4);
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -106,6 +108,7 @@
|
||||
}
|
||||
|
||||
&__dragAndDropText {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
text-transform: lowercase;
|
||||
align-self: center;
|
||||
|
||||
@@ -7,7 +7,6 @@ import React, { useEffect, useState } from 'react'
|
||||
import type { WhereBuilderProps } from './types.js'
|
||||
|
||||
import { useListQuery } from '../../providers/ListQuery/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { Condition } from './Condition/index.js'
|
||||
@@ -31,11 +30,11 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
const [reducedFields, setReducedColumns] = useState(() => reduceClientFields({ fields, i18n }))
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setReducedColumns(reduceClientFields({ fields, i18n }))
|
||||
}, [fields, i18n])
|
||||
|
||||
const { searchParams } = useSearchParams()
|
||||
const { handleWhereChange } = useListQuery()
|
||||
const { handleWhereChange, params } = useListQuery()
|
||||
const [shouldUpdateQuery, setShouldUpdateQuery] = React.useState(false)
|
||||
|
||||
// This handles initializing the where conditions from the search query (URL). That way, if you pass in
|
||||
@@ -67,7 +66,7 @@ export const WhereBuilder: React.FC<WhereBuilderProps> = (props) => {
|
||||
*/
|
||||
|
||||
const [conditions, setConditions] = React.useState(() => {
|
||||
const whereFromSearch = searchParams.where
|
||||
const whereFromSearch = params.where
|
||||
if (whereFromSearch) {
|
||||
if (validateWhereQuery(whereFromSearch)) {
|
||||
return whereFromSearch.or
|
||||
|
||||
@@ -284,7 +284,7 @@ const BlocksFieldComponent: React.FC<BlockFieldProps> = (props) => {
|
||||
|
||||
if (blockToRender) {
|
||||
const rowErrorCount = errorPaths.filter((errorPath) =>
|
||||
errorPath.startsWith(`${path}.${i}`),
|
||||
errorPath.startsWith(`${path}.${i}.`),
|
||||
).length
|
||||
return (
|
||||
<DraggableSortableItem disabled={disabled || !isSortable} id={row.id} key={row.id}>
|
||||
|
||||
@@ -62,6 +62,15 @@ export function UploadComponentHasMany(props: Props) {
|
||||
>
|
||||
{fileDocs.map(({ relationTo, value }, index) => {
|
||||
const id = String(value.id)
|
||||
const url: string = value.thumbnailURL || value.url
|
||||
let src: string
|
||||
|
||||
try {
|
||||
src = new URL(url, serverURL).toString()
|
||||
} catch {
|
||||
src = `${serverURL}${url}`
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableSortableItem disabled={!isSortable} id={id} key={id}>
|
||||
{(draggableSortableItemProps) => (
|
||||
@@ -100,7 +109,7 @@ export function UploadComponentHasMany(props: Props) {
|
||||
id={id}
|
||||
mimeType={value?.mimeType as string}
|
||||
onRemove={() => removeItem(index)}
|
||||
src={`${serverURL}${value.url}`}
|
||||
src={src}
|
||||
withMeta={false}
|
||||
x={value?.width as number}
|
||||
y={value?.height as number}
|
||||
|
||||
@@ -26,6 +26,15 @@ export function UploadComponentHasOne(props: Props) {
|
||||
const { relationTo, value } = fileDoc
|
||||
const id = String(value.id)
|
||||
|
||||
const url: string = value.thumbnailURL || value.url
|
||||
let src: string
|
||||
|
||||
try {
|
||||
src = new URL(url, serverURL).toString()
|
||||
} catch {
|
||||
src = `${serverURL}${url}`
|
||||
}
|
||||
|
||||
return (
|
||||
<UploadCard className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||
<RelationshipContent
|
||||
@@ -38,7 +47,7 @@ export function UploadComponentHasOne(props: Props) {
|
||||
id={id}
|
||||
mimeType={value?.mimeType as string}
|
||||
onRemove={onRemove}
|
||||
src={`${serverURL}${value.url}`}
|
||||
src={src}
|
||||
x={value?.width as number}
|
||||
y={value?.height as number}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
&__dropzoneContent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: base(0.4);
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -34,6 +36,7 @@
|
||||
}
|
||||
|
||||
&__dragAndDropText {
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
text-transform: lowercase;
|
||||
align-self: center;
|
||||
|
||||
@@ -9,6 +9,13 @@ type useThrottledEffect = (
|
||||
deps: React.DependencyList,
|
||||
) => void
|
||||
|
||||
/**
|
||||
* A hook that will throttle the execution of a callback function inside a useEffect.
|
||||
* This is useful for things like throttling loading states or other UI updates.
|
||||
* @param callback The callback function to be executed.
|
||||
* @param delay The delay in milliseconds to throttle the callback.
|
||||
* @param deps The dependencies to watch for changes.
|
||||
*/
|
||||
export const useThrottledEffect: useThrottledEffect = (callback, delay, deps = []) => {
|
||||
const lastRan = useRef(Date.now())
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PaginatedDocs, Where } from 'payload'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import { isNumber } from 'payload/shared'
|
||||
import * as qs from 'qs-esm'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { Column } from '../../elements/Table/index.js'
|
||||
|
||||
@@ -27,6 +27,7 @@ type ContextHandlers = {
|
||||
handleSearchChange?: (search: string) => Promise<void>
|
||||
handleSortChange?: (sort: string) => Promise<void>
|
||||
handleWhereChange?: (where: Where) => Promise<void>
|
||||
params: RefineOverrides
|
||||
}
|
||||
|
||||
export type ListQueryProps = {
|
||||
@@ -35,6 +36,11 @@ export type ListQueryProps = {
|
||||
readonly defaultLimit?: number
|
||||
readonly defaultSort?: string
|
||||
readonly modifySearchParams?: boolean
|
||||
/**
|
||||
* Used to manage the query params manually. If you pass this prop, the provider will not manage the query params from the searchParams.
|
||||
* Useful for modals or other components that need to manage the query params themselves.
|
||||
*/
|
||||
readonly params?: RefineOverrides
|
||||
readonly preferenceKey?: string
|
||||
} & PropHandlers
|
||||
|
||||
@@ -68,14 +74,15 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
handleSortChange: handleSortChangeFromProps,
|
||||
handleWhereChange: handleWhereChangeFromProps,
|
||||
modifySearchParams,
|
||||
params: paramsFromProps,
|
||||
preferenceKey,
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { setPreference } = usePreferences()
|
||||
const hasSetInitialParams = React.useRef(false)
|
||||
const { searchParams: currentQuery } = useSearchParams()
|
||||
const [params, setParams] = useState(paramsFromProps || currentQuery)
|
||||
|
||||
const refineListData = React.useCallback(
|
||||
const refineListData = useCallback(
|
||||
async (query: RefineOverrides) => {
|
||||
if (!modifySearchParams) {
|
||||
return
|
||||
@@ -114,10 +121,20 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
|
||||
router.replace(`${qs.stringify(params, { addQueryPrefix: true })}`)
|
||||
},
|
||||
[preferenceKey, modifySearchParams, router, setPreference, currentQuery],
|
||||
[
|
||||
modifySearchParams,
|
||||
currentQuery?.page,
|
||||
currentQuery?.limit,
|
||||
currentQuery?.search,
|
||||
currentQuery?.sort,
|
||||
currentQuery?.where,
|
||||
preferenceKey,
|
||||
router,
|
||||
setPreference,
|
||||
],
|
||||
)
|
||||
|
||||
const handlePageChange = React.useCallback(
|
||||
const handlePageChange = useCallback(
|
||||
async (arg: number) => {
|
||||
if (typeof handlePageChangeFromProps === 'function') {
|
||||
await handlePageChangeFromProps(arg)
|
||||
@@ -134,23 +151,25 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
await handlePerPageChangeFromProps(arg)
|
||||
}
|
||||
|
||||
await refineListData({ limit: String(arg) })
|
||||
await refineListData({ limit: String(arg), page: '1' })
|
||||
},
|
||||
[refineListData, handlePerPageChangeFromProps],
|
||||
)
|
||||
|
||||
const handleSearchChange = React.useCallback(
|
||||
const handleSearchChange = useCallback(
|
||||
async (arg: string) => {
|
||||
const search = arg === '' ? undefined : arg
|
||||
|
||||
if (typeof handleSearchChangeFromProps === 'function') {
|
||||
await handleSearchChangeFromProps(arg)
|
||||
await handleSearchChangeFromProps(search)
|
||||
}
|
||||
|
||||
await refineListData({ search: arg })
|
||||
await refineListData({ search })
|
||||
},
|
||||
[refineListData, handleSearchChangeFromProps],
|
||||
[handleSearchChangeFromProps, refineListData],
|
||||
)
|
||||
|
||||
const handleSortChange = React.useCallback(
|
||||
const handleSortChange = useCallback(
|
||||
async (arg: string) => {
|
||||
if (typeof handleSortChangeFromProps === 'function') {
|
||||
await handleSortChangeFromProps(arg)
|
||||
@@ -161,7 +180,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
[refineListData, handleSortChangeFromProps],
|
||||
)
|
||||
|
||||
const handleWhereChange = React.useCallback(
|
||||
const handleWhereChange = useCallback(
|
||||
async (arg: Where) => {
|
||||
if (typeof handleWhereChangeFromProps === 'function') {
|
||||
await handleWhereChangeFromProps(arg)
|
||||
@@ -172,8 +191,11 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
[refineListData, handleWhereChangeFromProps],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasSetInitialParams.current) {
|
||||
useEffect(() => {
|
||||
if (paramsFromProps) {
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setParams(paramsFromProps)
|
||||
} else {
|
||||
if (modifySearchParams) {
|
||||
let shouldUpdateQueryString = false
|
||||
|
||||
@@ -187,14 +209,15 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
shouldUpdateQueryString = true
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
||||
setParams(currentQuery)
|
||||
|
||||
if (shouldUpdateQueryString) {
|
||||
router.replace(`?${qs.stringify(currentQuery)}`)
|
||||
}
|
||||
}
|
||||
|
||||
hasSetInitialParams.current = true
|
||||
}
|
||||
}, [defaultSort, defaultLimit, router, modifySearchParams, currentQuery])
|
||||
}, [defaultSort, defaultLimit, router, modifySearchParams, currentQuery, paramsFromProps, params])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
@@ -205,6 +228,7 @@ export const ListQueryProvider: React.FC<ListQueryProps> = ({
|
||||
handleSearchChange,
|
||||
handleSortChange,
|
||||
handleWhereChange,
|
||||
params,
|
||||
refineListData,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -129,10 +129,16 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
|
||||
}
|
||||
let some = false
|
||||
let all = true
|
||||
Object.values(selected).forEach((val) => {
|
||||
all = all && val
|
||||
some = some || val
|
||||
})
|
||||
|
||||
if (!Object.values(selected).length) {
|
||||
all = false
|
||||
some = false
|
||||
} else {
|
||||
Object.values(selected).forEach((val) => {
|
||||
all = all && val
|
||||
some = some || val
|
||||
})
|
||||
}
|
||||
|
||||
if (all) {
|
||||
setSelectAll(SelectAllStatus.AllInPage)
|
||||
|
||||
@@ -220,9 +220,6 @@ export default buildConfigWithDefaults({
|
||||
slug: relationWithTitleSlug,
|
||||
},
|
||||
{
|
||||
admin: {
|
||||
useAsTitle: 'name',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
fields: [
|
||||
|
||||
@@ -4,9 +4,6 @@ import { defaultEmail, emailFieldsSlug } from './shared.js'
|
||||
|
||||
const EmailFields: CollectionConfig = {
|
||||
slug: emailFieldsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
defaultSort: 'id',
|
||||
fields: [
|
||||
{
|
||||
|
||||
31
test/fields/components/AfterNavLinks.tsx
Normal file
31
test/fields/components/AfterNavLinks.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import type { PayloadClientReactComponent, SanitizedConfig } from 'payload'
|
||||
|
||||
import { NavGroup, useConfig } from '@payloadcms/ui'
|
||||
import LinkImport from 'next/link.js'
|
||||
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
|
||||
import React from 'react'
|
||||
|
||||
const baseClass = 'after-nav-links'
|
||||
|
||||
export const AfterNavLinks: PayloadClientReactComponent<
|
||||
SanitizedConfig['admin']['components']['afterNavLinks'][0]
|
||||
> = () => {
|
||||
const {
|
||||
config: {
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
} = useConfig()
|
||||
|
||||
return (
|
||||
<NavGroup key="extra-links" label="Extra Links">
|
||||
{/* Open link to payload admin url */}
|
||||
{/* <Link href={`${adminRoute}/collections/uploads`}>Internal Payload Admin Link</Link> */}
|
||||
{/* Open link to payload admin url with prefiltered query */}
|
||||
<Link href={`${adminRoute}/collections/uploads?page=1&search=jpg&limit=10`}>
|
||||
Prefiltered Media
|
||||
</Link>
|
||||
</NavGroup>
|
||||
)
|
||||
}
|
||||
@@ -105,6 +105,9 @@ export default buildConfigWithDefaults({
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
components: {
|
||||
afterNavLinks: ['/components/AfterNavLinks.js#AfterNavLinks'],
|
||||
},
|
||||
custom: {
|
||||
client: {
|
||||
'new-value': 'client available',
|
||||
|
||||
@@ -3,8 +3,5 @@ import type { CollectionConfig } from 'payload'
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
fields: [],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user