Compare commits

..

6 Commits

Author SHA1 Message Date
Alessio Gravili
764c42eb80 fix: checkDocumentLockStatus transaction errors 2025-02-19 11:18:59 -07:00
Alessio Gravili
7626ff4635 test 2025-02-19 01:26:55 -07:00
Alessio Gravili
9595e74153 test 2025-02-19 01:17:35 -07:00
Alessio Gravili
58243a20e2 test 2025-02-19 01:15:54 -07:00
Alessio Gravili
a62d86fd15 Merge branch 'main' into fix/checkDocumentLockStatus 2025-02-19 00:45:11 -07:00
Alessio Gravili
0a664f9bac fix: db transaction errors caused by checkDocumentLockStatus 2025-02-18 21:21:23 -07:00
100 changed files with 963 additions and 2421 deletions

View File

@@ -1054,13 +1054,13 @@ import { usePayloadAPI } from '@payloadcms/ui'
const MyComponent: React.FC = () => {
// Fetch data from a collection item using its ID
const [{ data, isError, isLoading }, { setParams }] = usePayloadAPI(
const [{ data, error, isLoading }, { setParams }] = usePayloadAPI(
'/api/posts/123',
{ initialParams: { depth: 1 } }
)
if (isLoading) return <p>Loading...</p>
if (isError) return <p>Error occurred while fetching data.</p>
if (error) return <p>Error: {error.message}</p>
return (
<div>
@@ -1094,7 +1094,7 @@ The first item in the returned array is an object containing the following prope
| Property | Description |
| --------------- | -------------------------------------------------------- |
| **`data`** | The API response data. |
| **`isError`** | A boolean indicating whether the request failed. |
| **`error`** | If an error occurs, this contains the error object. |
| **`isLoading`** | A boolean indicating whether the request is in progress. |
The second item is an object with the following methods:

View File

@@ -242,6 +242,4 @@ You can customise the available list of timezones in the [global admin config](.
<Banner type='info'>
**Good to know:**
The date itself will be stored in UTC so it's up to you to handle the conversion to the user's timezone when displaying the date in your frontend.
Dates without a specific time are normalised to 12:00 in the selected timezone.
</Banner>

View File

@@ -50,7 +50,6 @@ export const MyRadioField: Field = {
| **`admin`** | Admin-specific configuration. [More details](#admin-options). |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |

View File

@@ -53,7 +53,6 @@ export const MySelectField: Field = {
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/database/postgres)). Auto-generated from name if not defined. |
| **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/typescript/generating-types#custom-field-interfaces) & [GraphQL type](/docs/graphql/graphql-schema#custom-field-schemas). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) |

View File

@@ -256,14 +256,18 @@ If you are using relationships or uploads in your front-end application, and you
// ...
// If your site is running on a different domain than your Payload server,
// This will allows requests to be made between the two domains
cors: [
'http://localhost:3001' // Your front-end application
],
cors: {
[
'http://localhost:3001' // Your front-end application
],
},
// If you are protecting resources behind user authentication,
// This will allow cookies to be sent between the two domains
csrf: [
'http://localhost:3001' // Your front-end application
],
csrf: {
[
'http://localhost:3001' // Your front-end application
],
},
}
```

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.24.0",
"version": "3.23.0",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.24.0",
"version": "3.23.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.24.0",
"version": "3.23.0",
"description": "The officially supported MongoDB database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.24.0",
"version": "3.23.0",
"description": "The officially supported Postgres database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-sqlite",
"version": "3.24.0",
"version": "3.23.0",
"description": "The officially supported SQLite database adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-vercel-postgres",
"version": "3.24.0",
"version": "3.23.0",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.24.0",
"version": "3.23.0",
"description": "A library of shared functions used by different payload database adapters",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-nodemailer",
"version": "3.24.0",
"version": "3.23.0",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/email-resend",
"version": "3.24.0",
"version": "3.23.0",
"description": "Payload Resend Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.24.0",
"version": "3.23.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.24.0",
"version": "3.23.0",
"description": "The official React SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-vue",
"version": "3.24.0",
"version": "3.23.0",
"description": "The official Vue SDK for Payload Live Preview",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview",
"version": "3.24.0",
"version": "3.23.0",
"description": "The official live preview JavaScript SDK for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/next",
"version": "3.24.0",
"version": "3.23.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",
@@ -96,6 +96,7 @@
"qs-esm": "7.0.2",
"react-diff-viewer-continued": "4.0.4",
"sass": "1.77.4",
"sonner": "^1.7.0",
"uuid": "10.0.0"
},
"devDependencies": {

View File

@@ -8,7 +8,6 @@ import {
MinimizeMaximizeIcon,
NumberField,
SetDocumentStepNav,
toast,
useConfig,
useDocumentInfo,
useLocale,
@@ -16,6 +15,7 @@ import {
} from '@payloadcms/ui'
import { useSearchParams } from 'next/navigation.js'
import * as React from 'react'
import { toast } from 'sonner'
import './index.scss'
import { LocaleSelector } from './LocaleSelector/index.js'

View File

@@ -1,4 +1,5 @@
'use client'
import type { OnConfirm } from '@payloadcms/ui'
import type { User } from 'payload'
import { Button, ConfirmationModal, toast, useModal, useTranslation } from '@payloadcms/ui'
@@ -14,46 +15,54 @@ export const ResetPreferences: React.FC<{
const { openModal } = useModal()
const { t } = useTranslation()
const handleResetPreferences = useCallback(async () => {
if (!user) {
return
}
const handleResetPreferences: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
if (!user) {
setConfirming(false)
closeConfirmationModal()
return
}
const stringifiedQuery = qs.stringify(
{
depth: 0,
where: {
user: {
id: {
equals: user.id,
const stringifiedQuery = qs.stringify(
{
depth: 0,
where: {
user: {
id: {
equals: user.id,
},
},
},
},
},
{ addQueryPrefix: true },
)
{ addQueryPrefix: true },
)
try {
const res = await fetch(`${apiRoute}/payload-preferences${stringifiedQuery}`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'DELETE',
})
try {
const res = await fetch(`${apiRoute}/payload-preferences${stringifiedQuery}`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'DELETE',
})
const json = await res.json()
const message = json.message
const json = await res.json()
const message = json.message
if (res.ok) {
toast.success(message)
} else {
toast.error(message)
if (res.ok) {
toast.success(message)
} else {
toast.error(message)
}
} catch (_err) {
// swallow error
} finally {
setConfirming(false)
closeConfirmationModal()
}
} catch (_err) {
// swallow error
}
}, [apiRoute, user])
},
[apiRoute, user],
)
return (
<Fragment>

View File

@@ -1,14 +1,13 @@
import { sanitizeID } from '@payloadcms/ui/shared'
import {
combineQueries,
extractAccessFromPermission,
type Payload,
type SanitizedCollectionConfig,
type SanitizedDocumentPermissions,
type SanitizedGlobalConfig,
type TypedUser,
import type {
Payload,
SanitizedCollectionConfig,
SanitizedDocumentPermissions,
SanitizedGlobalConfig,
TypedUser,
} from 'payload'
import { sanitizeID } from '@payloadcms/ui/shared'
type Args = {
collectionConfig?: SanitizedCollectionConfig
/**
@@ -135,18 +134,15 @@ export const getVersions = async ({
autosave: true,
},
user,
where: combineQueries(
{
and: [
{
parent: {
equals: id,
},
where: {
and: [
{
parent: {
equals: id,
},
],
},
extractAccessFromPermission(docPermissions.readVersions),
),
},
],
},
})
if (
@@ -162,28 +158,25 @@ export const getVersions = async ({
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
collection: collectionConfig.slug,
user,
where: combineQueries(
{
and: [
{
parent: {
equals: id,
},
where: {
and: [
{
parent: {
equals: id,
},
{
'version._status': {
equals: 'draft',
},
},
{
'version._status': {
equals: 'draft',
},
{
updatedAt: {
greater_than: publishedDoc.updatedAt,
},
},
{
updatedAt: {
greater_than: publishedDoc.updatedAt,
},
],
},
extractAccessFromPermission(docPermissions.readVersions),
),
},
],
},
}))
}
}
@@ -192,18 +185,15 @@ export const getVersions = async ({
collection: collectionConfig.slug,
depth: 0,
user,
where: combineQueries(
{
and: [
{
parent: {
equals: id,
},
where: {
and: [
{
parent: {
equals: id,
},
],
},
extractAccessFromPermission(docPermissions.readVersions),
),
},
],
},
}))
}
@@ -252,23 +242,20 @@ export const getVersions = async ({
depth: 0,
global: globalConfig.slug,
user,
where: combineQueries(
{
and: [
{
'version._status': {
equals: 'draft',
},
where: {
and: [
{
'version._status': {
equals: 'draft',
},
{
updatedAt: {
greater_than: publishedDoc.updatedAt,
},
},
{
updatedAt: {
greater_than: publishedDoc.updatedAt,
},
],
},
extractAccessFromPermission(docPermissions.readVersions),
),
},
],
},
}))
}
}

View File

@@ -1,11 +1,11 @@
'use client'
import type { OnConfirm } from '@payloadcms/ui'
import { getTranslation } from '@payloadcms/translations'
import {
Button,
ConfirmationModal,
PopupList,
toast,
useConfig,
useModal,
useRouteTransition,
@@ -14,6 +14,7 @@ import {
import { formatAdminURL, requests } from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js'
import React, { Fragment, useCallback, useState } from 'react'
import { toast } from 'sonner'
import type { Props } from './types.js'
@@ -74,21 +75,27 @@ const Restore: React.FC<Props> = ({
})
}
const handleRestore = useCallback(async () => {
const res = await requests.post(fetchURL, {
headers: {
'Accept-Language': i18n.language,
},
})
const handleRestore: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
const res = await requests.post(fetchURL, {
headers: {
'Accept-Language': i18n.language,
},
})
if (res.status === 200) {
const json = await res.json()
toast.success(json.message)
return startRouteTransition(() => router.push(redirectURL))
} else {
toast.error(t('version:problemRestoringVersion'))
}
}, [fetchURL, redirectURL, t, i18n, router, startRouteTransition])
setConfirming(false)
closeConfirmationModal()
if (res.status === 200) {
const json = await res.json()
toast.success(json.message)
startRouteTransition(() => router.push(redirectURL))
} else {
toast.error(t('version:problemRestoringVersion'))
}
},
[fetchURL, redirectURL, t, i18n, router, startRouteTransition],
)
return (
<Fragment>

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/payload-cloud",
"version": "3.24.0",
"version": "3.23.0",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "3.24.0",
"version": "3.23.0",
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
"keywords": [
"admin panel",

View File

@@ -1,17 +0,0 @@
import type { AccessResult } from '../config/types.js'
import type { Permission } from './index.js'
export const extractAccessFromPermission = (hasPermission: boolean | Permission): AccessResult => {
if (typeof hasPermission === 'boolean') {
return hasPermission
}
const { permission, where } = hasPermission
if (!permission) {
return false
}
if (where && typeof where === 'object') {
return where
}
return true
}

View File

@@ -1 +0,0 @@
export { lt } from '@payloadcms/translations/languages/lt'

View File

@@ -1045,13 +1045,6 @@ export type SelectField = {
*/
enumName?: DBIdentifierName
hasMany?: boolean
/** Customize generated GraphQL and Typescript schema names.
* By default, it is bound to the collection.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique amongst collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
options: Option[]
type: 'select'
} & (
@@ -1069,7 +1062,7 @@ export type SelectField = {
export type SelectFieldClient = {
admin?: AdminClient & Pick<SelectField['admin'], 'isClearable' | 'isSortable'>
} & FieldBaseClient &
Pick<SelectField, 'hasMany' | 'interfaceName' | 'options' | 'type'>
Pick<SelectField, 'hasMany' | 'options' | 'type'>
type SharedRelationshipProperties = {
filterOptions?: FilterOptions
@@ -1276,13 +1269,6 @@ export type RadioField = {
* Customize the DB enum name
*/
enumName?: DBIdentifierName
/** Customize generated GraphQL and Typescript schema names.
* By default, it is bound to the collection.
*
* This is useful if you would like to generate a top level type to share amongst collections/fields.
* **Note**: Top level types can collide, ensure they are unique amongst collections, arrays, groups, blocks, tabs.
*/
interfaceName?: string
options: Option[]
type: 'radio'
validate?: RadioFieldValidation
@@ -1291,7 +1277,7 @@ export type RadioField = {
export type RadioFieldClient = {
admin?: AdminClient & Pick<RadioField['admin'], 'layout'>
} & FieldBaseClient &
Pick<RadioField, 'interfaceName' | 'options' | 'type'>
Pick<RadioField, 'options' | 'type'>
type BlockFields = {
[key: string]: any

View File

@@ -968,7 +968,6 @@ interface RequestContext {
export interface DatabaseAdapter extends BaseDatabaseAdapter {}
export type { Payload, RequestContext }
export { executeAuthStrategies } from './auth/executeAuthStrategies.js'
export { extractAccessFromPermission } from './auth/extractAccessFromPermission.js'
export { getAccessResults } from './auth/getAccessResults.js'
export { getFieldsToSign } from './auth/getFieldsToSign.js'
export * from './auth/index.js'
@@ -985,7 +984,6 @@ export { resetPasswordOperation } from './auth/operations/resetPassword.js'
export { unlockOperation } from './auth/operations/unlock.js'
export { verifyEmailOperation } from './auth/operations/verifyEmail.js'
export { JWTAuthentication } from './auth/strategies/jwt.js'
export type {
AuthStrategyFunction,
AuthStrategyFunctionArgs,
@@ -1006,8 +1004,8 @@ export type {
} from './auth/types.js'
export { generateImportMap } from './bin/generateImportMap/index.js'
export type { ImportMap } from './bin/generateImportMap/index.js'
export type { ImportMap } from './bin/generateImportMap/index.js'
export { genImportMapIterateFields } from './bin/generateImportMap/iterateFields.js'
export {
@@ -1054,6 +1052,7 @@ export type {
TypeWithID,
TypeWithTimestamps,
} from './collections/config/types.js'
export { createDataloaderCacheKey, getDataLoader } from './collections/dataloader.js'
export { countOperation } from './collections/operations/count.js'
export { createOperation } from './collections/operations/create.js'
@@ -1076,8 +1075,8 @@ export {
serverOnlyConfigProperties,
type UnsanitizedClientConfig,
} from './config/client.js'
export { defaults } from './config/defaults.js'
export { sanitizeConfig } from './config/sanitize.js'
export type * from './config/types.js'
export { combineQueries } from './database/combineQueries.js'

View File

@@ -63,7 +63,7 @@ export const checkDocumentLockStatus = async ({
collection: 'payload-locked-documents',
limit: 1,
pagination: false,
req,
//req,
sort: '-updatedAt',
where: lockedDocumentQuery,
})

View File

@@ -496,14 +496,6 @@ export function fieldsToJSONSchema(
enum: buildOptionEnums(field.options),
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
}
}
break
}
@@ -665,15 +657,6 @@ export function fieldsToJSONSchema(
fieldSchema.enum = optionEnums
}
}
if (field.interfaceName) {
interfaceNameDefinitions.set(field.interfaceName, fieldSchema)
fieldSchema = {
$ref: `#/definitions/${field.interfaceName}`,
}
}
break
}
break

View File

@@ -48,10 +48,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
let docBeingAccessed: EntityDoc | Promise<EntityDoc | undefined> | undefined
async function getEntityDoc({
operation,
where,
}: { operation?: AllOperations; where?: Where } = {}): Promise<EntityDoc | undefined> {
async function getEntityDoc({ where }: { where?: Where } = {}): Promise<EntityDoc | undefined> {
if (!entity.slug) {
return undefined
}
@@ -69,28 +66,18 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
if (type === 'collection' && id) {
if (typeof where === 'object') {
const options = {
const paginatedRes = await payload.find({
collection: entity.slug,
depth: 0,
fallbackLocale: null,
limit: 1,
locale,
overrideAccess: true,
req,
}
if (operation === 'readVersions') {
const paginatedRes = await payload.findVersions({
...options,
where: combineQueries(where, { parent: { equals: id } }),
})
return paginatedRes?.docs?.[0] || undefined
}
const paginatedRes = await payload.find({
...options,
pagination: false,
req,
where: combineQueries(where, { id: { equals: id } }),
})
return paginatedRes?.docs?.[0] || undefined
}
@@ -129,9 +116,7 @@ export async function getEntityPolicies<T extends Args>(args: T): Promise<Return
if (typeof accessResult === 'object' && !disableWhere) {
mutablePolicies[operation] = {
permission:
id || type === 'global'
? !!(await getEntityDoc({ operation, where: accessResult }))
: true,
id || type === 'global' ? !!(await getEntityDoc({ where: accessResult })) : true,
where: accessResult,
}
} else if (mutablePolicies[operation]?.permission !== false) {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud-storage",
"version": "3.24.0",
"version": "3.23.0",
"description": "The official cloud storage plugin for Payload CMS",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.24.0",
"version": "3.23.0",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-multi-tenant",
"version": "3.24.0",
"version": "3.23.0",
"description": "Multi Tenant plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,4 +1,4 @@
import type { CollectionConfig } from 'payload'
import type { Access, CollectionConfig } from 'payload'
import type { MultiTenantPluginConfig } from '../types.js'
@@ -34,14 +34,21 @@ export const addCollectionAccess = <ConfigType>({
fieldName,
userHasAccessToAllTenants,
}: Args<ConfigType>): void => {
collectionAccessKeys.forEach((key) => {
if (!collection?.access) {
collection.access = {}
}
collectionAccessKeys.reduce<{
[key in (typeof collectionAccessKeys)[number]]?: Access
}>((acc, key) => {
if (!collection.access) {
collection.access = {}
return acc
}
collection.access[key] = withTenantAccess<ConfigType>({
accessFunction: collection.access?.[key],
fieldName: key === 'readVersions' ? `version.${fieldName}` : fieldName,
fieldName,
userHasAccessToAllTenants,
})
})
return acc
}, {})
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.24.0",
"version": "3.23.0",
"description": "The official Nested Docs plugin for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-redirects",
"version": "3.24.0",
"version": "3.23.0",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.24.0",
"version": "3.23.0",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,5 +1,7 @@
'use client'
import type { OnConfirm } from '@payloadcms/ui'
import {
ConfirmationModal,
Popup,
@@ -24,7 +26,7 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
searchCollections,
searchSlug,
}) => {
const { openModal } = useModal()
const { closeModal, openModal } = useModal()
const { config } = useConfig()
@@ -39,33 +41,46 @@ export const ReindexButtonClient: React.FC<ReindexButtonProps> = ({
const openConfirmModal = useCallback(() => openModal(confirmReindexModalSlug), [openModal])
const handleReindexSubmit = useCallback(async () => {
if (!reindexCollections.length) {
return
}
try {
const res = await fetch(`${config.routes.api}/${searchSlug}/reindex?locale=${locale.code}`, {
body: JSON.stringify({
collections: reindexCollections,
}),
method: 'POST',
})
const { message } = (await res.json()) as { message: string }
if (!res.ok) {
toast.error(message)
} else {
toast.success(message)
return router.refresh()
const handleReindexSubmit: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
if (!reindexCollections.length) {
setConfirming(false)
closeConfirmationModal()
return
}
} catch (_err: unknown) {
// swallow error, toast shown above
} finally {
setReindexCollections([])
}
}, [reindexCollections, router, searchSlug, locale, config])
try {
const res = await fetch(
`${config.routes.api}/${searchSlug}/reindex?locale=${locale.code}`,
{
body: JSON.stringify({
collections: reindexCollections,
}),
method: 'POST',
},
)
setConfirming(false)
closeConfirmationModal()
const { message } = (await res.json()) as { message: string }
if (!res.ok) {
toast.error(message)
} else {
toast.success(message)
router.refresh()
}
} catch (_err: unknown) {
// swallow error, toast shown above
} finally {
setConfirming(false)
closeConfirmationModal()
setReindexCollections([])
}
},
[reindexCollections, router, searchSlug, locale, config],
)
const handleShowConfirmModal = useCallback(
(collections: string | string[] = searchCollections) => {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-sentry",
"version": "3.24.0",
"version": "3.23.0",
"description": "Sentry plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.24.0",
"version": "3.23.0",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -18,7 +18,6 @@ import { hu } from './hu.js'
import { it } from './it.js'
import { ja } from './ja.js'
import { ko } from './ko.js'
import { lt } from './lt.js'
import { my } from './my.js'
import { nb } from './nb.js'
import { nl } from './nl.js'
@@ -57,7 +56,6 @@ export const translations = {
it,
ja,
ko,
lt,
my,
nb,
nl,

View File

@@ -1,28 +0,0 @@
import type { GenericTranslationsObject } from '@payloadcms/translations'
export const lt: GenericTranslationsObject = {
$schema: './translation-schema.json',
'plugin-seo': {
almostThere: 'Beveik baigta',
autoGenerate: 'Automatinis generavimas',
bestPractices: 'geriausios praktikos',
characterCount: '{{current}}/{{minLength}}-{{maxLength}} simbolių, ',
charactersLeftOver: '{{characters}} likusių simbolių',
charactersToGo: '{{characters}} simbolių liko',
charactersTooMany: '{{characters}} per daug simbolių',
checksPassing: '{{current}}/{{max}} tikrinimų sėkmingi',
good: 'Gerai',
imageAutoGenerationTip: 'Automatinis generavimas paims pasirinktą pagrindinį vaizdą.',
lengthTipDescription:
'Šis tekstas turi būti tarp {{minLength}} ir {{maxLength}} simbolių. Norėdami gauti pagalbos rašant kokybiškus meta aprašus, žiūrėkite ',
lengthTipTitle:
'Šis tekstas turi būti tarp {{minLength}} ir {{maxLength}} simbolių. Norėdami gauti pagalbos rašant kokybiškus meta pavadinimus, žiūrėkite ',
missing: 'Trūksta',
noImage: 'Nėra vaizdo',
preview: 'Peržiūra',
previewDescription:
'Tikrųjų paieškos rezultatų gali skirtis priklausomai nuo turinio ir paieškos svarbos.',
tooLong: 'Per ilgas',
tooShort: 'Per trumpas',
},
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.24.0",
"version": "3.23.0",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.24.0",
"version": "3.23.0",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {
@@ -354,8 +354,8 @@
"@lexical/react": "0.21.0",
"@lexical/rich-text": "0.21.0",
"@lexical/selection": "0.21.0",
"@lexical/table": "0.21.0",
"@lexical/utils": "0.21.0",
"@lexical/table": "0.21.0",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/uuid": "10.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.24.0",
"version": "3.23.0",
"description": "The officially supported Slate richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-azure",
"version": "3.24.0",
"version": "3.23.0",
"description": "Payload storage adapter for Azure Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-gcs",
"version": "3.24.0",
"version": "3.23.0",
"description": "Payload storage adapter for Google Cloud Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-s3",
"version": "3.24.0",
"version": "3.23.0",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-uploadthing",
"version": "3.24.0",
"version": "3.23.0",
"description": "Payload storage adapter for uploadthing",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/storage-vercel-blob",
"version": "3.24.0",
"version": "3.23.0",
"description": "Payload storage adapter for Vercel Blob Storage",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.24.0",
"version": "3.23.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -18,7 +18,6 @@ import { hu } from '../languages/hu.js'
import { it } from '../languages/it.js'
import { ja } from '../languages/ja.js'
import { ko } from '../languages/ko.js'
import { lt } from '../languages/lt.js'
import { my } from '../languages/my.js'
import { nb } from '../languages/nb.js'
import { nl } from '../languages/nl.js'
@@ -57,7 +56,6 @@ export const translations = {
it,
ja,
ko,
lt,
my,
nb,
nl,

View File

@@ -75,10 +75,6 @@ export const importDateFNSLocale = async (locale: string): Promise<Locale> => {
case 'ko':
result = (await import('date-fns/locale/ko')).ko
break
case 'lt':
result = (await import('date-fns/locale/lt')).lt
break
case 'nb':
result = (await import('date-fns/locale/nb')).nb

View File

@@ -1,513 +0,0 @@
import type { DefaultTranslationsObject, Language } from '../types.js'
export const ltTranslations: DefaultTranslationsObject = {
authentication: {
account: 'Paskyra',
accountOfCurrentUser: 'Dabartinio vartotojo paskyra',
accountVerified: 'Sąskaita sėkmingai patvirtinta.',
alreadyActivated: 'Jau aktyvuota',
alreadyLoggedIn: 'Jau prisijungęs',
apiKey: 'API raktas',
authenticated: 'Autentifikuotas',
backToLogin: 'Grįžti į prisijungimą',
beginCreateFirstUser: 'Pradėkite, sukurdami savo pirmąjį vartotoją.',
changePassword: 'Keisti slaptažodį',
checkYourEmailForPasswordReset:
'Jei šis el. pašto adresas yra susijęs su paskyra, netrukus gausite instrukcijas, kaip atstatyti savo slaptažodį. Jei laiško nesimate savo gautiesiųjų dėžutėje, patikrinkite savo šlamšto ar nereikalingų laiškų aplanką.',
confirmGeneration: 'Patvirtinkite generavimą',
confirmPassword: 'Patvirtinkite slaptažodį',
createFirstUser: 'Sukurkite pirmąjį vartotoją',
emailNotValid: 'Pateiktas el. paštas negalioja',
emailOrUsername: 'El. paštas arba vartotojo vardas',
emailSent: 'El. paštas išsiųstas',
emailVerified: 'El. paštas sėkmingai patvirtintas.',
enableAPIKey: 'Įgalinti API raktą',
failedToUnlock: 'Nepavyko atrakinti',
forceUnlock: 'Priverstinis atrakinimas',
forgotPassword: 'Pamiršote slaptažodį',
forgotPasswordEmailInstructions:
'Prašome įvesti savo el. paštą žemiau. Gausite el. laišką su instrukcijomis, kaip atstatyti savo slaptažodį.',
forgotPasswordQuestion: 'Pamiršote slaptažodį?',
forgotPasswordUsernameInstructions:
'Prašome įvesti savo vartotojo vardą žemiau. Instrukcijos, kaip atstatyti slaptažodį, bus išsiųstos į el. pašto adresą, susietą su jūsų vartotojo vardu.',
generate: 'Generuoti',
generateNewAPIKey: 'Sukurkite naują API raktą',
generatingNewAPIKeyWillInvalidate:
'Sugeneruojant naują API raktą, bus <1>anuliuotas</1> ankstesnis raktas. Ar tikrai norite tęsti?',
lockUntil: 'Užrakinti iki',
logBackIn: 'Prisijunkite vėl',
loggedIn: 'Norėdami prisijungti kitu vartotoju, turėtumėte iš pradžių <0>atsijungti</0>.',
loggedInChangePassword:
'Norėdami pakeisti slaptažodį, eikite į savo <0>paskyrą</0> ir ten redaguokite savo slaptažodį.',
loggedOutInactivity: 'Jūs buvote atjungtas dėl neveiklumo.',
loggedOutSuccessfully: 'Sėkmingai atsijungėte.',
loggingOut: 'Atsijungimas...',
login: 'Prisijungti',
loginAttempts: 'Prisijungimo bandymai',
loginUser: 'Prisijungti vartotojui',
loginWithAnotherUser:
'Norėdami prisijungti su kitu vartotoju, turėtumėte iš pradžių <0>atsijungti</0>.',
logOut: 'Atsijungti',
logout: 'Atsijungti',
logoutSuccessful: 'Sėkmingai atsijungta.',
logoutUser: 'Atjungti vartotoją',
newAccountCreated:
'Jums ką tik buvo sukurta nauja paskyra, kad galėtumėte prisijungti prie <a href="{{serverURL}}">{{serverURL}}</a> Prašome paspausti ant šios nuorodos arba įklijuoti apačioje esantį URL į savo naršyklę, kad patvirtintumėte savo el. pašto adresą: <a href="{{verificationURL}}">{{verificationURL}}</a><br> Patvirtinę savo el. pašto adresą, sėkmingai galėsite prisijungti.',
newAPIKeyGenerated: 'Sugeneruotas naujas API raktas.',
newPassword: 'Naujas slaptažodis',
passed: 'Autentifikacija sėkminga',
passwordResetSuccessfully: 'Slaptažodis sėkmingai atnaujintas.',
resetPassword: 'Atstatyti slaptažodį',
resetPasswordExpiration: 'Atstatyti slaptažodžio galiojimo laiką',
resetPasswordToken: 'Slaptažodžio atkūrimo žetonas',
resetYourPassword: 'Atstatykite savo slaptažodį',
stayLoggedIn: 'Likite prisijungę',
successfullyRegisteredFirstUser: 'Sėkmingai užregistruotas pirmas vartotojas.',
successfullyUnlocked: 'Sėkmingai atrakinta',
tokenRefreshSuccessful: 'Žetonų atnaujinimas sėkmingas.',
unableToVerify: 'Negalima patikrinti',
username: 'Vartotojo vardas',
usernameNotValid: 'Pateiktas vartotojo vardas yra netinkamas',
verified: 'Patvirtinta',
verifiedSuccessfully: 'Sėkmingai patvirtinta',
verify: 'Patikrinkite',
verifyUser: 'Patvirtinti vartotoją',
verifyYourEmail: 'Patvirtinkite savo el. paštą',
youAreInactive:
'Jūs kurį laiką neveikėte ir netrukus būsite automatiškai atjungtas dėl jūsų pačių saugumo. Ar norėtumėte likti prisijungęs?',
youAreReceivingResetPassword:
'Gavote šį pranešimą, nes jūs (arba kažkas kitas) paprašėte atstatyti slaptažodį savo paskyrai. Norėdami užbaigti procesą, spustelėkite šią nuorodą arba įklijuokite ją į savo naršyklę:',
youDidNotRequestPassword:
'Jei to neprašėte, prašome ignoruoti šį el. laišką ir jūsų slaptažodis išliks nepakeistas.',
},
error: {
accountAlreadyActivated: 'Ši paskyra jau aktyvuota.',
autosaving: 'Šio dokumento automatinio išsaugojimo metu kilo problema.',
correctInvalidFields: 'Prašome ištaisyti neteisingus laukus.',
deletingFile: 'Įvyko klaida trinant failą.',
deletingTitle:
'Įvyko klaida bandant ištrinti {{title}}. Patikrinkite savo ryšį ir bandykite dar kartą.',
emailOrPasswordIncorrect: 'Pateiktas el. pašto adresas arba slaptažodis yra neteisingi.',
followingFieldsInvalid_one: 'Šis laukas yra netinkamas:',
followingFieldsInvalid_other: 'Šie laukai yra neteisingi:',
incorrectCollection: 'Neteisinga kolekcija',
invalidFileType: 'Netinkamas failo tipas',
invalidFileTypeValue: 'Neteisingas failo tipas: {{value}}',
invalidRequestArgs: 'Netinkami argumentai perduoti užklausoje: {{args}}',
loadingDocument: 'Įvyko klaida įkeliant dokumentą, kurio ID yra {{id}}.',
localesNotSaved_one: 'Negalima išsaugoti šios lokalės:',
localesNotSaved_other: 'Šios lokalės negalėjo būti išsaugotos:',
logoutFailed: 'Atsijungimas nepavyko.',
missingEmail: 'Trūksta el. pašto.',
missingIDOfDocument: 'Trūksta dokumento, kurį reikia atnaujinti, ID.',
missingIDOfVersion: 'Trūksta versijos ID.',
missingRequiredData: 'Trūksta reikalingų duomenų.',
noFilesUploaded: 'Neįkelta jokių failų.',
noMatchedField: 'Nerasta atitinkamo lauko „{{label}}“',
notAllowedToAccessPage: 'Jums neleidžiama prieiti prie šio puslapio.',
notAllowedToPerformAction: 'Jums neleidžiama atlikti šio veiksmo.',
notFound: 'Pageidaujamas išteklius nerasta.',
noUser: 'Nėra vartotojo',
previewing: 'Šiam dokumentui peržiūrėti kilo problema.',
problemUploadingFile: 'Failo įkelti nepavyko dėl problemos.',
tokenInvalidOrExpired: 'Žetonas yra neteisingas arba jo galiojimas pasibaigė.',
tokenNotProvided: 'Žetonas nesuteiktas.',
unableToDeleteCount: 'Negalima ištrinti {{count}} iš {{total}} {{label}}.',
unableToReindexCollection:
'Klaida perindeksuojant rinkinį {{collection}}. Operacija nutraukta.',
unableToUpdateCount: 'Nepavyko atnaujinti {{count}} iš {{total}} {{label}}.',
unauthorized: 'Neleistina, turite būti prisijungęs, kad galėtumėte teikti šį prašymą.',
unauthorizedAdmin:
'Neleidžiama, šis vartotojas neturi prieigos prie administratoriaus panelės.',
unknown: 'Įvyko nežinoma klaida.',
unPublishingDocument: 'Šio dokumento nepublikuojant kildavo problema.',
unspecific: 'Įvyko klaida.',
userEmailAlreadyRegistered: 'Vartotojas su nurodytu el. paštu jau yra užregistruotas.',
userLocked: 'Šis vartotojas užrakintas dėl per daug nepavykusių prisijungimo bandymų.',
usernameAlreadyRegistered: 'Vartotojas su nurodytu vartotojo vardu jau užregistruotas.',
usernameOrPasswordIncorrect: 'Pateiktas vartotojo vardas arba slaptažodis yra neteisingas.',
valueMustBeUnique: 'Vertė turi būti unikalu.',
verificationTokenInvalid: 'Patvirtinimo kodas yra negaliojantis.',
},
fields: {
addLabel: 'Pridėkite {{žymė}}',
addLink: 'Pridėti nuorodą',
addNew: 'Pridėti naują',
addNewLabel: 'Pridėti naują {{žymę}}',
addRelationship: 'Pridėti santykį',
addUpload: 'Pridėti Įkelti',
block: 'blokas',
blocks: 'blokai',
blockType: 'Blokas Tipas',
chooseBetweenCustomTextOrDocument:
'Pasirinkite tarp pasirinkimo įvesti tinkintą tekstą URL arba nuorodos į kitą dokumentą.',
chooseDocumentToLink: 'Pasirinkite dokumentą, prie kurio norite prisegti.',
chooseFromExisting: 'Pasirinkite iš esamų',
chooseLabel: 'Pasirinkite {{žymė}}',
collapseAll: 'Sutraukti viską',
customURL: 'Pasirinktinis URL',
editLabelData: 'Redaguoti {{label}} duomenis',
editLink: 'Redaguoti nuorodą',
editRelationship: 'Redaguoti santykius',
enterURL: 'Įveskite URL',
internalLink: 'Vidinis nuorodos',
itemsAndMore: '{{items}} ir dar {{count}}',
labelRelationship: '{{label}} Santykiai',
latitude: 'Platuma',
linkedTo: 'Susijęs su <0>{{label}}</0>',
linkType: 'Nuorodos tipas',
longitude: 'Ilgumažė',
newLabel: 'Naujas {{žymė}}',
openInNewTab: 'Atidaryti naujame skirtuke',
passwordsDoNotMatch: 'Slaptažodžiai nesutampa.',
relatedDocument: 'Susijęs dokumentas',
relationTo: 'Santykis su',
removeRelationship: 'Pašalinti ryšį',
removeUpload: 'Pašalinti įkėlimą',
saveChanges: 'Išsaugoti pakeitimus',
searchForBlock: 'Ieškokite bloko',
selectExistingLabel: 'Pasirinkite esamą {{žymę}}',
selectFieldsToEdit: 'Pasirinkite laukus, kuriuos norite redaguoti',
showAll: 'Rodyti viską',
swapRelationship: 'Apkeičiamas santykis',
swapUpload: 'Keitimo įkėlimas',
textToDisplay: 'Rodyti tekstą',
toggleBlock: 'Perjungti bloką',
uploadNewLabel: 'Įkelti naują {{label}}',
},
general: {
aboutToDelete: 'Jūs ketinate ištrinti {{label}} <1>{{title}}</1>. Ar esate tikri?',
aboutToDeleteCount_many: 'Jūs ketinate ištrinti {{count}} {{label}}',
aboutToDeleteCount_one: 'Jūs ketinate ištrinti {{count}} {{label}}',
aboutToDeleteCount_other: 'Jūs ketinate ištrinti {{count}} {{label}}',
addBelow: 'Pridėti žemiau',
addFilter: 'Pridėti filtrą',
adminTheme: 'Admin temos',
all: 'Visi',
allCollections: 'Visos kolekcijos',
and: 'Ir',
anotherUser: 'Kitas vartotojas',
anotherUserTakenOver: 'Kitas naudotojas perėmė šio dokumento redagavimą.',
applyChanges: 'Taikyti pakeitimus',
ascending: 'Kylantis',
automatic: 'Automatinis',
backToDashboard: 'Atgal į informacinę skydelį',
cancel: 'Atšaukti',
changesNotSaved:
'Jūsų pakeitimai nebuvo išsaugoti. Jei dabar išeisite, prarasite savo pakeitimus.',
clearAll: 'Išvalyti viską',
close: 'Uždaryti',
collapse: 'Susikolimas',
collections: 'Kolekcijos',
columns: 'Stulpeliai',
columnToSort: 'Rūšiuoti stulpelį',
confirm: 'Patvirtinti',
confirmCopy: 'Patvirtinkite kopiją',
confirmDeletion: 'Patvirtinkite šalinimą',
confirmDuplication: 'Patvirtinkite dubliavimą',
confirmReindex: 'Perindeksuoti visas {{kolekcijas}}?',
confirmReindexAll: 'Perindeksuoti visas kolekcijas?',
confirmReindexDescription:
'Tai pašalins esamus indeksus ir iš naujo indeksuos dokumentus kolekcijose {{collections}}.',
confirmReindexDescriptionAll:
'Tai pašalins esamas indeksus ir perindeksuos dokumentus visose kolekcijose.',
copied: 'Nukopijuota',
copy: 'Kopijuoti',
copying: 'Kopijavimas',
copyWarning:
'Jūs ketinate perrašyti {{to}} į {{from}} šildymui {{label}} {{title}}. Ar esate tikri?',
create: 'Sukurti',
created: 'Sukurta',
createdAt: 'Sukurta',
createNew: 'Sukurti naują',
createNewLabel: 'Sukurti naują {{label}}',
creating: 'Kuriant',
creatingNewLabel: 'Kuriamas naujas {{label}}',
currentlyEditing:
'šiuo metu redaguoja šį dokumentą. Jei perimsite, jie bus užblokuoti ir negalės toliau redaguoti, o taip pat gali prarasti neišsaugotus pakeitimus.',
custom: 'Paprastas',
dark: 'Tamsus',
dashboard: 'Prietaisų skydelis',
delete: 'Ištrinti',
deletedCountSuccessfully: 'Sėkmingai ištrinta {{count}} {{label}}.',
deletedSuccessfully: 'Sėkmingai ištrinta.',
deleting: 'Trinama...',
depth: 'Gylis',
descending: 'Mažėjantis',
deselectAllRows: 'Atžymėkite visas eilutes',
document: 'Dokumentas',
documentLocked: 'Dokumentas užrakintas',
documents: 'Dokumentai',
duplicate: 'Dublikatas',
duplicateWithoutSaving: 'Dubliuoti be įrašytų pakeitimų',
edit: 'Redaguoti',
editAll: 'Redaguoti viską',
editedSince: 'Redaguota nuo',
editing: 'Redagavimas',
editingLabel_many: 'Redaguojama {{count}} {{label}}',
editingLabel_one: 'Redaguojama {{count}} {{label}}',
editingLabel_other: 'Redaguojamas {{count}} {{label}}',
editingTakenOver: 'Redagavimas perimtas',
editLabel: 'Redaguoti {{žymę}}',
email: 'El. paštas',
emailAddress: 'El. pašto adresas',
enterAValue: 'Įveskite reikšmę',
error: 'Klaida',
errors: 'Klaidos',
fallbackToDefaultLocale: 'Grįžkite į numatytąją vietovę',
false: 'Netiesa',
filter: 'Filtruoti',
filters: 'Filtrai',
filterWhere: 'Filtruoti {{label}}, kur',
globals: 'Globalai',
goBack: 'Grįžkite',
isEditing: 'redaguoja',
language: 'Kalba',
lastModified: 'Paskutinį kartą modifikuota',
leaveAnyway: 'Vis tiek išeikite',
leaveWithoutSaving: 'Išeikite neišsaugoję',
light: 'Šviesa',
livePreview: 'Tiesioginė peržiūra',
loading: 'Kraunama',
locale: 'Lokalė',
locales: 'Lokalės',
menu: 'Meniu',
moreOptions: 'Daugiau parinkčių',
moveDown: 'Perkelti žemyn',
moveUp: 'Pakilti',
newPassword: 'Naujas slaptažodis',
next: 'Toliau',
noDateSelected: 'Pasirinktos datos nėra',
noFiltersSet: 'Nenustatyti jokie filtrai',
noLabel: '<Ne {{label}}>',
none: 'Jokios',
noOptions: 'Jokių variantų',
noResults:
'Nerasta jokių {{label}}. Arba dar nėra sukurtų {{label}}, arba jie neatitinka nurodytų filtrų aukščiau.',
notFound: 'Nerasta',
nothingFound: 'Nieko nerasta',
noUpcomingEventsScheduled: 'Nėra suplanuotų būsimų renginių.',
noValue: 'Nėra vertės',
of: 'apie',
only: 'Tik',
open: 'Atidaryti',
or: 'Arba',
order: 'Užsakyti',
overwriteExistingData: 'Perrašyti esamus lauko duomenis',
pageNotFound: 'Puslapis nerastas',
password: 'Slaptažodis',
payloadSettings: 'Payload nustatymai',
perPage: 'Puslapyje: {{limit}}',
previous: 'Ankstesnis',
reindex: 'Perindeksuoti',
reindexingAll: 'Perindeksuojamos visos {{kolekcijos}}.',
remove: 'Pašalinti',
reset: 'Atstatyti',
resetPreferences: 'Atstatyti nuostatas',
resetPreferencesDescription: 'Tai atstatys visas jūsų nuostatas į numatytąsias reikšmes.',
resettingPreferences: 'Nustatymų atstatymas.',
row: 'Eilutė',
rows: 'Eilutės',
save: 'Išsaugoti',
saving: 'Išsaugoti...',
schedulePublishFor: 'Suplanuokite publikaciją „{{title}}“',
searchBy: 'Ieškokite pagal {{žymę}}',
selectAll: 'Pasirinkite visus {{count}} {{label}}',
selectAllRows: 'Pasirinkite visas eilutes',
selectedCount: '{{count}} {{label}} pasirinkta',
selectValue: 'Pasirinkite reikšmę',
showAllLabel: 'Rodyti visus {{label}}',
sorryNotFound: 'Atsiprašau - nėra nieko, atitinkančio jūsų užklausą.',
sort: 'Rūšiuoti',
sortByLabelDirection: 'Rūšiuoti pagal {{label}} {{direction}}',
stayOnThisPage: 'Likite šiame puslapyje',
submissionSuccessful: 'Pateikimas sėkmingas.',
submit: 'Pateikti',
submitting: 'Pateikiama...',
success: 'Sėkmė',
successfullyCreated: '{{label}} sėkmingai sukurtas.',
successfullyDuplicated: '{{label}} sėkmingai dubliuotas.',
successfullyReindexed:
'Sėkmingai perindeksuota {{count}} iš {{total}} dokumentų iš {{collections}}',
takeOver: 'Perimti',
thisLanguage: 'Lietuvių',
time: 'Laikas',
timezone: 'Laiko juosta',
titleDeleted: '{{label}} "{{title}}" sėkmingai ištrinta.',
true: 'Tiesa',
unauthorized: 'Neleistinas',
unsavedChanges: 'Turite neišsaugotų pakeitimų. Išsaugokite arba atmestkite prieš tęsdami.',
unsavedChangesDuplicate: 'Jūs turite neišsaugotų pakeitimų. Ar norėtumėte tęsti dubliavimą?',
untitled: 'Neužpavadinamas',
upcomingEvents: 'Artimieji renginiai',
updatedAt: 'Atnaujinta',
updatedCountSuccessfully: '{{count}} {{label}} sėkmingai atnaujinta.',
updatedSuccessfully: 'Sėkmingai atnaujinta.',
updating: 'Atnaujinimas',
uploading: 'Įkeliama',
uploadingBulk: 'Įkeliamas {{current}} iš {{total}}',
user: 'Vartotojas',
username: 'Vartotojo vardas',
users: 'Vartotojai',
value: 'Vertė',
viewReadOnly: 'Peržiūrėti tik skaitymui',
welcome: 'Sveiki',
},
localization: {
cannotCopySameLocale: 'Negalima kopijuoti į tą pačią vietovę',
copyFrom: 'Kopijuoti iš',
copyFromTo: 'Kopijavimas iš {{from}} į {{to}}',
copyTo: 'Kopijuoti į',
copyToLocale: 'Kopijuoti į vietovę',
localeToPublish: 'Publikuoti lokacijoje',
selectLocaleToCopy: 'Pasirinkite lokalės kopijavimui',
},
operators: {
contains: 'yra',
equals: 'lygus',
exists: 'egzistuoja',
intersects: 'susikerta',
isGreaterThan: 'yra didesnis nei',
isGreaterThanOrEqualTo: 'yra didesnis arba lygus',
isIn: 'yra',
isLessThan: 'yra mažiau nei',
isLessThanOrEqualTo: 'yra mažiau arba lygu',
isLike: 'yra panašu',
isNotEqualTo: 'nelygu',
isNotIn: 'nėra',
near: 'šalia',
within: 'viduje',
},
upload: {
addFile: 'Pridėti failą',
addFiles: 'Pridėti failus',
bulkUpload: 'Masinis įkėlimas',
crop: 'Pasėlis',
cropToolDescription:
'Temkite pasirinktos srities kampus, nubrėžkite naują sritį arba koreguokite žemiau esančias reikšmes.',
dragAndDrop: 'Temkite ir numeskite failą',
dragAndDropHere: 'arba nuvilkite failą čia',
editImage: 'Redaguoti vaizdą',
fileName: 'Failo pavadinimas',
fileSize: 'Failo dydis',
filesToUpload: 'Įkelti failai',
fileToUpload: 'Įkelti failą',
focalPoint: 'Fokuso Taškas',
focalPointDescription:
'Temkite fokusavimo tašką tiesiogiai peržiūroje arba reguliuokite žemiau esančias reikšmes.',
height: 'Aukštis',
lessInfo: 'Mažiau informacijos',
moreInfo: 'Daugiau informacijos',
pasteURL: 'Įklijuokite URL',
previewSizes: 'Peržiūros dydžiai',
selectCollectionToBrowse: 'Pasirinkite kolekciją, kurią norėtumėte naršyti',
selectFile: 'Pasirinkite failą',
setCropArea: 'Nustatykite pjovimo plotą',
setFocalPoint: 'Nustatyti fokuso tašką',
sizes: 'Dydžiai',
sizesFor: 'Dydžiai skirti {{žymei}}',
width: 'Plotis',
},
validation: {
emailAddress: 'Įveskite galiojantį el. pašto adresą.',
enterNumber: 'Įveskite galiojantį skaičių.',
fieldHasNo: 'Šiame lauke nėra {{label}}',
greaterThanMax:
'{{value}} yra didesnė nei leidžiama maksimali {{label}} reikšmė, kuri yra {{max}}.',
invalidInput: 'Šis laukas turi netinkamą įvestį.',
invalidSelection: 'Šiame lauke yra netinkamas pasirinkimas.',
invalidSelections: 'Šiame lauke yra šios netinkamos parinktys:',
lessThanMin:
'{{value}} yra mažesnė nei leidžiama minimali {{label}} reikšmė, kuri yra {{min}}.',
limitReached: 'Pasiektas limitas, galima pridėti tik {{max}} daiktus.',
longerThanMin:
'Ši reikšmė turi būti ilgesnė nei minimalus simbolių skaičius, kuris yra {{minLength}} simboliai.',
notValidDate: '"{{value}}" nėra galiojanti data.',
required: 'Šis laukas yra privalomas.',
requiresAtLeast: 'Šis laukas reikalauja bent {{count}} {{label}}.',
requiresNoMoreThan: 'Šiame laukelyje gali būti ne daugiau kaip {{count}} {{label}}.',
requiresTwoNumbers: 'Šiame lauke reikia įvesti du skaičius.',
shorterThanMax: 'Ši reikšmė turi būti trumpesnė nei maksimalus {{maxLength}} simbolių ilgis.',
timezoneRequired: 'Reikia nustatyti laiko juostą.',
trueOrFalse: 'Šis laukas gali būti lygus tik „true“ ar „false“.',
username:
'Įveskite galiojantį vartotojo vardą. Galima naudoti raides, skaičius, brūkšnelius, taškus ir pabraukimus.',
validUploadID: 'Šis laukas nėra tinkamas įkėlimo ID.',
},
version: {
type: 'Įveskite',
aboutToPublishSelection: 'Jūs ketinate išleisti visus {{label}} išrinktame. Ar esate tikri?',
aboutToRestore:
'Jūs ketinate atkurti šį {{label}} dokumentą į būklę, kurioje jis buvo {{versionDate}}.',
aboutToRestoreGlobal:
'Jūs ketinate atkurti visuotinę {{label}} būklę, kokia ji buvo {{versionDate}}.',
aboutToRevertToPublished:
'Jūs ketinate atšaukti šio dokumento pakeitimus ir grįžti prie publikuotos versijos. Ar esate įsitikinęs?',
aboutToUnpublish: 'Jūs ketinate panaikinti šio dokumento publikavimą. Ar esate tikri?',
aboutToUnpublishSelection:
'Jūs ketinate atšaukti visų {{label}} pasirinkime. Ar esate įsitikinęs?',
autosave: 'Automatinis išsaugojimas',
autosavedSuccessfully: 'Sėkmingai automatiškai išsaugota.',
autosavedVersion: 'Automatiškai išsaugota versija',
changed: 'Pakeistas',
changedFieldsCount_one: '{{count}} pakeistas laukas',
changedFieldsCount_other: '{{count}} pakeisti laukai',
compareVersion: 'Palyginkite versiją su:',
confirmPublish: 'Patvirtinkite publikaciją',
confirmRevertToSaved: 'Patvirtinkite grįžimą į įrašytą',
confirmUnpublish: 'Patvirtinkite nepublikavimą',
confirmVersionRestoration: 'Patvirtinkite versijos atkūrimą',
currentDocumentStatus: 'Dabartinis {{docStatus}} dokumentas',
currentDraft: 'Dabartinis projektas',
currentPublishedVersion: 'Dabartinė publikuota versija',
draft: 'Projektas',
draftSavedSuccessfully: 'Juosmuo sėkmingai išsaugotas.',
lastSavedAgo: 'Paskutinį kartą išsaugota prieš {{distance}}',
modifiedOnly: 'Tik modifikuotas',
noFurtherVersionsFound: 'Nerasta daugiau versijų',
noRowsFound: 'Nerasta {{žymė}}',
noRowsSelected: 'Pasirinkta ne viena {{žymė}}',
preview: 'Peržiūra',
previouslyPublished: 'Ankstesnė publikacija',
problemRestoringVersion: 'Buvo problema atkuriant šią versiją',
publish: 'Paskelbti',
publishAllLocales: 'Publikuokite visus lokalizacijas',
publishChanges: 'Paskelbti pakeitimus',
published: 'Paskelbta',
publishIn: 'Paskelbti {{locale}}',
publishing: 'Leidyba',
restoreAsDraft: 'Atkurti kaip juodraštį',
restoredSuccessfully: 'Sėkmingai atkurtas.',
restoreThisVersion: 'Atkurti šią versiją',
restoring: 'Atkuriamas...',
reverting: 'Grįžtama...',
revertToPublished: 'Grįžti prie publikuotojo',
saveDraft: 'Išsaugoti juodraštį',
scheduledSuccessfully: 'Sėkmingai suplanuota.',
schedulePublish: 'Suplanuokite publikaciją',
selectLocales: 'Pasirinkite lokales, kurias norėtumėte rodyti',
selectVersionToCompare: 'Pasirinkite versiją, kurią norite palyginti',
showingVersionsFor: 'Rodomos versijos:',
showLocales: 'Rodyti lokalizacijas:',
status: 'Būsena',
unpublish: 'Nebepublikuoti',
unpublishing: 'Nebepublikuojama...',
version: 'Versija',
versionCount_many: 'Rasta {{count}} versijų',
versionCount_none: 'Nerasta jokių versijų',
versionCount_one: 'Rasta {{count}} versija',
versionCount_other: 'Rasta {{count}} versijų',
versionCreatedOn: '{{version}} sukurtas:',
versionID: 'Versijos ID',
versions: 'Versijos',
viewingVersion: 'Peržiūrėkite versiją {{entityLabel}} {{documentTitle}}',
viewingVersionGlobal: 'Peržiūrint visuotinę {{entityLabel}} versiją',
viewingVersions: 'Peržiūrint versijas {{entityLabel}} {{documentTitle}}',
viewingVersionsGlobal: 'Peržiūrėti globalaus {{entityLabel}} versijas',
},
}
export const lt: Language = {
dateFNSKey: 'lt',
translations: ltTranslations,
}

View File

@@ -23,7 +23,6 @@ type DateFNSKeys =
| 'it'
| 'ja'
| 'ko'
| 'lt'
| 'nb'
| 'nl'
| 'pl'

View File

@@ -21,7 +21,6 @@ export const acceptedLanguages = [
'it',
'ja',
'ko',
'lt',
'my',
'nb',
'nl',
@@ -86,6 +85,7 @@ export const acceptedLanguages = [
* 'ku-Arab',
* 'ky-Cyrl',
* 'lb',
* 'lt',
* 'lv',
* 'mi-Latn',
* 'mk',

View File

@@ -36,11 +36,8 @@ const useClientPlugin = {
result.outputFiles.forEach((file) => {
let contents = file.text
if (!file.path.endsWith('.map')) {
contents = contents.replace(directiveRegex, '') // Remove existing use client directives
contents = directive + '\n' + contents // Prepend our use client directive
}
contents = contents.replace(directiveRegex, '') // Remove existing use client directives
contents = directive + '\n' + contents // Prepend our use client directive
if (originalWrite) {
const filePath = path.join(build.initialOptions.outdir, path.basename(file.path))

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/ui",
"version": "3.24.0",
"version": "3.23.0",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -68,20 +68,11 @@
background-color: var(--bg-color);
box-shadow: var(--box-shadow);
border-radius: $style-radius-m;
border-left: 1px solid var(--theme-bg);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
align-items: center;
html:not([dir='RTL']) & {
border-left: 1px solid var(--theme-bg);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
html[dir='RTL'] & {
border-right: 1px solid var(--theme-bg);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:hover,
&:focus-visible,
&:focus,
@@ -245,15 +236,8 @@
}
&--withPopup .btn {
html:not([dir='RTL']) & {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
html[dir='RTL'] & {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&--style-icon-label,

View File

@@ -185,7 +185,6 @@ export const Button: React.FC<Props> = (props) => {
className={disabled && !enableSubMenu ? `${baseClass}--popup-disabled` : ''}
disabled={disabled && !enableSubMenu}
horizontalAlign="right"
id={`${id}-popup`}
noBackground
render={({ close }) => SubMenuPopupContent({ close: () => close() })}
size="large"

View File

@@ -1,6 +1,6 @@
'use client'
import { Modal, useModal } from '@faceless-ui/modal'
import React, { useCallback } from 'react'
import React from 'react'
import { useEditDepth } from '../../providers/EditDepth/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
@@ -10,31 +10,34 @@ import './index.scss'
const baseClass = 'confirmation-modal'
export type OnConfirm = (args: {
closeConfirmationModal: () => void
setConfirming: (state: boolean) => void
}) => Promise<void> | void
export type OnCancel = () => void
export type ConfirmationModalProps = {
body: React.ReactNode
cancelLabel?: string
className?: string
confirmingLabel?: string
confirmLabel?: string
heading: React.ReactNode
modalSlug: string
onCancel?: OnCancel
onConfirm: () => Promise<void> | void
onConfirm: OnConfirm
}
export function ConfirmationModal(props: ConfirmationModalProps) {
const {
body,
cancelLabel,
className,
confirmingLabel,
confirmLabel,
heading,
modalSlug,
onCancel: onCancelFromProps,
onConfirm: onConfirmFromProps,
onCancel,
onConfirm,
} = props
const editDepth = useEditDepth()
@@ -44,32 +47,9 @@ export function ConfirmationModal(props: ConfirmationModalProps) {
const { closeModal } = useModal()
const { t } = useTranslation()
const onConfirm = useCallback(async () => {
if (!confirming) {
setConfirming(true)
if (typeof onConfirmFromProps === 'function') {
await onConfirmFromProps()
}
setConfirming(false)
closeModal(modalSlug)
}
}, [confirming, onConfirmFromProps, closeModal, modalSlug])
const onCancel = useCallback(() => {
if (!confirming) {
closeModal(modalSlug)
if (typeof onCancelFromProps === 'function') {
onCancelFromProps()
}
}
}, [confirming, onCancelFromProps, closeModal, modalSlug])
return (
<Modal
className={[baseClass, className].filter(Boolean).join(' ')}
className={baseClass}
slug={modalSlug}
style={{
zIndex: drawerZBase + editDepth,
@@ -85,15 +65,36 @@ export function ConfirmationModal(props: ConfirmationModalProps) {
buttonStyle="secondary"
disabled={confirming}
id="confirm-cancel"
onClick={onCancel}
onClick={
confirming
? undefined
: () => {
closeModal(modalSlug)
if (typeof onCancel === 'function') {
onCancel()
}
}
}
size="large"
type="button"
>
{cancelLabel || t('general:cancel')}
</Button>
<Button id="confirm-action" onClick={onConfirm} size="large">
<Button
id="confirm-action"
onClick={() => {
if (!confirming) {
setConfirming(true)
void onConfirm({
closeConfirmationModal: () => closeModal(modalSlug),
setConfirming: (state) => setConfirming(state),
})
}
}}
size="large"
>
{confirming
? confirmingLabel || `${t('general:loading')}...`
? confirmingLabel || t('general:loading')
: confirmLabel || t('general:confirm')}
</Button>
</div>

View File

@@ -19,7 +19,6 @@ const baseClass = 'date-time-picker'
const DatePicker: React.FC<Props> = (props) => {
const {
id,
displayFormat: customDisplayFormat,
maxDate,
maxTime,
@@ -114,7 +113,7 @@ const DatePicker: React.FC<Props> = (props) => {
}, [i18n.language, i18n.dateFNS])
return (
<div className={classes} id={id}>
<div className={classes}>
<div className={`${baseClass}__icon-wrap`}>
{dateTimePickerProps.selected && (
<button

View File

@@ -1,7 +1,6 @@
import type { DayPickerProps, SharedProps, TimePickerProps } from 'payload'
export type Props = {
id?: string
onChange?: (val: Date) => void
placeholder?: string
readOnly?: boolean

View File

@@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation.js'
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { OnConfirm } from '../ConfirmationModal/index.js'
import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js'
import { useForm } from '../../forms/Form/context.js'
@@ -68,79 +69,86 @@ export const DeleteDocument: React.FC<Props> = (props) => {
toast.error(t('error:deletingTitle', { title }))
}, [t, title])
const handleDelete = useCallback(async () => {
setModified(false)
const handleDelete: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
setModified(false)
try {
await requests
.delete(`${serverURL}${api}/${collectionSlug}/${id}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
})
.then(async (res) => {
try {
const json = await res.json()
try {
await requests
.delete(`${serverURL}${api}/${collectionSlug}/${id}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
})
.then(async (res) => {
try {
const json = await res.json()
setConfirming(false)
closeConfirmationModal()
if (res.status < 400) {
toast.success(
t('general:titleDeleted', {
label: getTranslation(singularLabel, i18n),
title,
}) || json.message,
)
if (redirectAfterDelete) {
return startRouteTransition(() =>
router.push(
formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}`,
}),
),
if (res.status < 400) {
toast.success(
t('general:titleDeleted', {
label: getTranslation(singularLabel, i18n),
title,
}) || json.message,
)
if (redirectAfterDelete) {
return startRouteTransition(() =>
router.push(
formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}`,
}),
),
)
}
if (typeof onDelete === 'function') {
await onDelete({ id, collectionConfig })
}
return
}
if (typeof onDelete === 'function') {
await onDelete({ id, collectionConfig })
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return
return false
} catch (_err) {
return addDefaultError()
}
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return false
} catch (_err) {
return addDefaultError()
}
})
} catch (_err) {
return addDefaultError()
}
}, [
setModified,
serverURL,
api,
collectionSlug,
id,
t,
singularLabel,
addDefaultError,
i18n,
title,
router,
adminRoute,
redirectAfterDelete,
onDelete,
collectionConfig,
startRouteTransition,
])
})
} catch (_err) {
setConfirming(false)
closeConfirmationModal()
return addDefaultError()
}
},
[
setModified,
serverURL,
api,
collectionSlug,
id,
t,
singularLabel,
addDefaultError,
i18n,
title,
router,
adminRoute,
redirectAfterDelete,
onDelete,
collectionConfig,
startRouteTransition,
],
)
if (id) {
return (

View File

@@ -8,6 +8,8 @@ import * as qs from 'qs-esm'
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { OnConfirm } from '../ConfirmationModal/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js'
@@ -52,87 +54,94 @@ export const DeleteMany: React.FC<Props> = (props) => {
toast.error(t('error:unknown'))
}, [t])
const handleDelete = useCallback(async () => {
const queryWithSearch = mergeListSearchAndWhere({
collectionConfig: collection,
search: searchParams.get('search'),
})
const queryString = getQueryParams(queryWithSearch)
await requests
.delete(`${serverURL}${api}/${slug}${queryString}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
const handleDelete: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
const queryWithSearch = mergeListSearchAndWhere({
collectionConfig: collection,
search: searchParams.get('search'),
})
.then(async (res) => {
try {
const json = await res.json()
const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular
const queryString = getQueryParams(queryWithSearch)
if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:deletedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
await requests
.delete(`${serverURL}${api}/${slug}${queryString}`, {
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
})
.then(async (res) => {
try {
const json = await res.json()
setConfirming(false)
closeConfirmationModal()
if (json?.errors.length > 0) {
const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular
if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:deletedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
}
toggleAll()
router.replace(
qs.stringify(
{
page: selectAll ? '1' : undefined,
},
{ addQueryPrefix: true },
),
)
clearRouteCache()
return null
}
if (json.errors) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
} else {
return addDefaultError()
}
toggleAll()
router.replace(
qs.stringify(
{
page: selectAll ? '1' : undefined,
},
{ addQueryPrefix: true },
),
)
clearRouteCache()
return null
}
if (json.errors) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
} else {
return false
} catch (_err) {
setConfirming(false)
closeConfirmationModal()
return addDefaultError()
}
return false
} catch (_err) {
return addDefaultError()
}
})
}, [
searchParams,
addDefaultError,
api,
getQueryParams,
i18n,
plural,
router,
selectAll,
serverURL,
singular,
slug,
t,
toggleAll,
clearRouteCache,
collection,
])
})
},
[
searchParams,
addDefaultError,
api,
getQueryParams,
i18n,
plural,
router,
selectAll,
serverURL,
singular,
slug,
t,
toggleAll,
clearRouteCache,
collection,
],
)
if (selectAll === SelectAllStatus.None || !hasDeletePermission) {
return null

View File

@@ -8,6 +8,7 @@ import { useRouter } from 'next/navigation.js'
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { OnConfirm } from '../ConfirmationModal/index.js'
import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js'
import { useForm, useFormModified } from '../../forms/Form/context.js'
@@ -57,77 +58,91 @@ export const DuplicateDocument: React.FC<Props> = ({
const modalSlug = `duplicate-${id}`
const duplicate = useCallback(async () => {
setRenderModal(true)
const duplicate = useCallback(
async ({ onResponse }: { onResponse?: () => void } = {}) => {
setRenderModal(true)
await requests
.post(
`${serverURL}${apiRoute}/${slug}/${id}/duplicate${locale?.code ? `?locale=${locale.code}` : ''}`,
{
body: JSON.stringify({}),
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
credentials: 'include',
await requests
.post(
`${serverURL}${apiRoute}/${slug}/${id}/duplicate${locale?.code ? `?locale=${locale.code}` : ''}`,
{
body: JSON.stringify({}),
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
credentials: 'include',
},
},
},
)
.then(async (res) => {
const { doc, errors, message } = await res.json()
)
.then(async (res) => {
const { doc, errors, message } = await res.json()
if (typeof onResponse === 'function') {
onResponse()
}
if (res.status < 400) {
toast.success(
message ||
t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }),
)
if (res.status < 400) {
toast.success(
message ||
t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }),
)
setModified(false)
setModified(false)
if (redirectAfterDuplicate) {
return startRouteTransition(() =>
router.push(
formatAdminURL({
adminRoute,
path: `/collections/${slug}/${doc.id}${locale?.code ? `?locale=${locale.code}` : ''}`,
}),
),
if (redirectAfterDuplicate) {
return startRouteTransition(() =>
router.push(
formatAdminURL({
adminRoute,
path: `/collections/${slug}/${doc.id}${locale?.code ? `?locale=${locale.code}` : ''}`,
}),
),
)
}
if (typeof onDuplicate === 'function') {
void onDuplicate({ collectionConfig, doc })
}
} else {
toast.error(
errors?.[0].message ||
message ||
t('error:unspecific', { label: getTranslation(singularLabel, i18n) }),
)
}
})
},
[
locale,
serverURL,
apiRoute,
slug,
id,
i18n,
t,
singularLabel,
onDuplicate,
redirectAfterDuplicate,
setModified,
router,
adminRoute,
collectionConfig,
startRouteTransition,
],
)
if (typeof onDuplicate === 'function') {
void onDuplicate({ collectionConfig, doc })
}
} else {
toast.error(
errors?.[0].message ||
message ||
t('error:unspecific', { label: getTranslation(singularLabel, i18n) }),
)
}
const onConfirm: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
setRenderModal(false)
await duplicate({
onResponse: () => {
setConfirming(false)
closeConfirmationModal()
},
})
}, [
locale,
serverURL,
apiRoute,
slug,
id,
i18n,
t,
singularLabel,
onDuplicate,
redirectAfterDuplicate,
setModified,
router,
adminRoute,
collectionConfig,
startRouteTransition,
])
const onConfirm = useCallback(async () => {
setRenderModal(false)
await duplicate()
}, [duplicate])
},
[duplicate],
)
return (
<React.Fragment>

View File

@@ -3,6 +3,8 @@ import { useModal } from '@faceless-ui/modal'
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { OnConfirm } from '../ConfirmationModal/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { Button } from '../Button/index.js'
@@ -23,11 +25,16 @@ export function GenerateConfirmation(props: GenerateConfirmationProps) {
const modalSlug = `generate-confirmation-${id}`
const handleGenerate = useCallback(() => {
setKey()
toast.success(t('authentication:newAPIKeyGenerated'))
highlightField(true)
}, [highlightField, setKey, t])
const handleGenerate: OnConfirm = useCallback(
({ closeConfirmationModal, setConfirming }) => {
setKey()
toast.success(t('authentication:newAPIKeyGenerated'))
highlightField(true)
setConfirming(false)
closeConfirmationModal()
},
[highlightField, setKey, t],
)
return (
<React.Fragment>

View File

@@ -1,7 +1,7 @@
'use client'
import React, { useCallback } from 'react'
import type { OnCancel } from '../ConfirmationModal/index.js'
import type { OnCancel, OnConfirm } from '../ConfirmationModal/index.js'
import { useForm, useFormModified } from '../../forms/Form/index.js'
import { useAuth } from '../../providers/Auth/index.js'
@@ -36,8 +36,10 @@ export const LeaveWithoutSaving: React.FC = () => {
closeModal(modalSlug)
}, [closeModal])
const onConfirm = useCallback(() => {
const onConfirm: OnConfirm = useCallback(({ closeConfirmationModal, setConfirming }) => {
setHasAccepted(true)
setConfirming(false)
closeConfirmationModal()
}, [])
return (

View File

@@ -21,7 +21,6 @@ export const LocalizerLabel: React.FC<{
<div
aria-label={ariaLabel || t('general:locale')}
className={[baseClass, className].filter(Boolean).join(' ')}
data-locale={locale ? locale.code : undefined}
>
<div className={`${baseClass}__label`}>{`${t('general:locale')}:`}&nbsp;</div>
<div className={`${baseClass}__current`}>

View File

@@ -61,17 +61,12 @@ export const Localizer: React.FC<{
<Fragment>
{localeOptionLabel}
&nbsp;
<span
className={`${baseClass}__locale-code`}
data-locale={localeOption.code}
>
<span className={`${baseClass}__locale-code`}>
{`(${localeOption.code})`}
</span>
</Fragment>
) : (
<span className={`${baseClass}__locale-code`} data-locale={localeOption.code}>
{localeOptionLabel}
</span>
<span className={`${baseClass}__locale-code`}>{localeOptionLabel}</span>
)}
</PopupList.Button>
)

View File

@@ -25,7 +25,6 @@ export type PopupProps = {
disabled?: boolean
forceOpen?: boolean
horizontalAlign?: 'center' | 'left' | 'right'
id?: string
initActive?: boolean
noBackground?: boolean
onToggleOpen?: (active: boolean) => void
@@ -38,7 +37,6 @@ export type PopupProps = {
export const Popup: React.FC<PopupProps> = (props) => {
const {
id,
boundingRef,
button,
buttonClassName,
@@ -170,7 +168,7 @@ export const Popup: React.FC<PopupProps> = (props) => {
.join(' ')
return (
<div className={classes} id={id}>
<div className={classes}>
<div className={`${baseClass}__trigger-wrap`}>
{showOnHover ? (
<div

View File

@@ -36,7 +36,6 @@ import { buildUpcomingColumns } from './buildUpcomingColumns.js'
const baseClass = 'schedule-publish'
type Props = {
defaultType?: PublishType
slug: string
}
@@ -45,7 +44,7 @@ const defaultLocaleOption = {
value: 'all',
}
export const ScheduleDrawer: React.FC<Props> = ({ slug, defaultType }) => {
export const ScheduleDrawer: React.FC<Props> = ({ slug }) => {
const { toggleModal } = useModal()
const {
config: {
@@ -61,7 +60,7 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug, defaultType }) => {
const { id, collectionSlug, globalSlug, title } = useDocumentInfo()
const { i18n, t } = useTranslation()
const { schedulePublish } = useServerFunctions()
const [type, setType] = React.useState<PublishType>(defaultType || 'publish')
const [type, setType] = React.useState<PublishType>('publish')
const [date, setDate] = React.useState<Date>()
const [timezone, setTimezone] = React.useState<string>(defaultTimezone)
const [locale, setLocale] = React.useState<{ label: string; value: string }>(defaultLocaleOption)
@@ -314,9 +313,8 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug, defaultType }) => {
</li>
</ul>
<br />
<FieldLabel label={t('general:time')} path={'time'} required />
<FieldLabel label={t('general:time')} required />
<DatePickerField
id="time"
minDate={new Date()}
onChange={(e) => onChangeDate(e)}
pickerAppearance="dayAndTime"
@@ -345,13 +343,7 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug, defaultType }) => {
</React.Fragment>
)}
<div className={`${baseClass}__actions`}>
<Button
buttonStyle="primary"
disabled={processing}
id="scheduled-publish-save"
onClick={handleSave}
type="button"
>
<Button buttonStyle="primary" disabled={processing} onClick={handleSave} type="button">
{t('general:save')}
</Button>
{processing ? <span>{t('general:saving')}</span> : null}

View File

@@ -73,10 +73,7 @@ export function PublishButton({ label: labelProp }: PublishButtonClientProps) {
entityConfig?.versions?.drafts.schedulePublish
const canSchedulePublish = Boolean(
scheduledPublishEnabled &&
hasPublishPermission &&
(globalSlug || (collectionSlug && id)) &&
!modified,
scheduledPublishEnabled && hasPublishPermission && (globalSlug || (collectionSlug && id)),
)
const operation = useOperation()
@@ -212,10 +209,7 @@ export function PublishButton({ label: labelProp }: PublishButtonClientProps) {
<React.Fragment>
{canSchedulePublish && (
<PopupList.ButtonGroup key="schedule-publish">
<PopupList.Button
id="schedule-publish"
onClick={() => [toggleModal(drawerSlug), close()]}
>
<PopupList.Button onClick={() => [toggleModal(drawerSlug), close()]}>
{t('version:schedulePublish')}
</PopupList.Button>
</PopupList.ButtonGroup>
@@ -236,12 +230,7 @@ export function PublishButton({ label: labelProp }: PublishButtonClientProps) {
>
{localization ? defaultLabel : label}
</FormSubmit>
{canSchedulePublish && isModalOpen(drawerSlug) && (
<ScheduleDrawer
defaultType={!hasNewerVersions ? 'unpublish' : 'publish'}
slug={drawerSlug}
/>
)}
{canSchedulePublish && isModalOpen(drawerSlug) && <ScheduleDrawer slug={drawerSlug} />}
</React.Fragment>
)
}

View File

@@ -8,6 +8,8 @@ import * as qs from 'qs-esm'
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { OnConfirm } from '../ConfirmationModal/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js'
@@ -52,80 +54,87 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
toast.error(t('error:unknown'))
}, [t])
const handlePublish = useCallback(async () => {
await requests
.patch(
`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}&draft=true`,
{
body: JSON.stringify({
_status: 'published',
}),
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
const handlePublish: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
await requests
.patch(
`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}&draft=true`,
{
body: JSON.stringify({
_status: 'published',
}),
headers: {
'Accept-Language': i18n.language,
'Content-Type': 'application/json',
},
},
},
)
.then(async (res) => {
try {
const json = await res.json()
)
.then(async (res) => {
try {
const json = await res.json()
setConfirming(false)
closeConfirmationModal()
const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular
const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular
if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:updatedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:updatedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
}
router.replace(
qs.stringify(
{
...parseSearchParams(searchParams),
page: selectAll ? '1' : undefined,
},
{ addQueryPrefix: true },
),
)
clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this
return null
}
router.replace(
qs.stringify(
{
...parseSearchParams(searchParams),
page: selectAll ? '1' : undefined,
},
{ addQueryPrefix: true },
),
)
clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this
return null
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return false
} catch (_err) {
setConfirming(false)
closeConfirmationModal()
return addDefaultError()
}
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return false
} catch (_err) {
return addDefaultError()
}
})
}, [
serverURL,
api,
slug,
getQueryParams,
i18n,
plural,
singular,
t,
router,
searchParams,
selectAll,
clearRouteCache,
addDefaultError,
])
})
},
[
serverURL,
api,
slug,
getQueryParams,
i18n,
plural,
singular,
t,
router,
searchParams,
selectAll,
clearRouteCache,
addDefaultError,
],
)
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {
return null

View File

@@ -3,6 +3,8 @@ import { useModal } from '@faceless-ui/modal'
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { OnConfirm } from '../ConfirmationModal/index.js'
import { useForm } from '../../forms/Form/context.js'
import { useConfig } from '../../providers/Config/index.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
@@ -53,7 +55,10 @@ export const Status: React.FC = () => {
}
const performAction = useCallback(
async (action: 'revert' | 'unpublish') => {
async (
action: 'revert' | 'unpublish',
{ closeConfirmationModal, setConfirming }: Parameters<OnConfirm>[0],
) => {
let url
let method
let body
@@ -95,6 +100,9 @@ export const Status: React.FC = () => {
},
})
setConfirming(false)
closeConfirmationModal()
if (res.status === 200) {
let data
const json = await res.json()
@@ -163,7 +171,7 @@ export const Status: React.FC = () => {
confirmingLabel={t('version:unpublishing')}
heading={t('version:confirmUnpublish')}
modalSlug={unPublishModalSlug}
onConfirm={() => performAction('unpublish')}
onConfirm={(args) => performAction('unpublish', args)}
/>
</React.Fragment>
)}
@@ -183,7 +191,7 @@ export const Status: React.FC = () => {
confirmingLabel={t('version:reverting')}
heading={t('version:confirmRevertToSaved')}
modalSlug={revertModalSlug}
onConfirm={() => performAction('revert')}
onConfirm={(args) => performAction('revert', args)}
/>
</React.Fragment>
)}

View File

@@ -2,7 +2,7 @@
import { useRouter } from 'next/navigation.js'
import React, { useCallback } from 'react'
import type { OnCancel } from '../ConfirmationModal/index.js'
import type { OnCancel, OnConfirm } from '../ConfirmationModal/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
@@ -29,16 +29,22 @@ export const StayLoggedInModal: React.FC = () => {
const { t } = useTranslation()
const { startRouteTransition } = useRouteTransition()
const onConfirm = useCallback(() => {
return startRouteTransition(() =>
router.push(
formatAdminURL({
adminRoute,
path: logoutRoute,
}),
),
)
}, [router, startRouteTransition, adminRoute, logoutRoute])
const onConfirm: OnConfirm = useCallback(
({ closeConfirmationModal, setConfirming }) => {
setConfirming(false)
closeConfirmationModal()
startRouteTransition(() =>
router.push(
formatAdminURL({
adminRoute,
path: logoutRoute,
}),
),
)
},
[router, startRouteTransition, adminRoute, logoutRoute],
)
const onCancel: OnCancel = useCallback(() => {
refreshCookie()

View File

@@ -8,6 +8,8 @@ import * as qs from 'qs-esm'
import React, { useCallback } from 'react'
import { toast } from 'sonner'
import type { OnConfirm } from '../ConfirmationModal/index.js'
import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js'
@@ -51,78 +53,87 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
toast.error(t('error:unknown'))
}, [t])
const handleUnpublish = useCallback(async () => {
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()
const handleUnpublish: OnConfirm = useCallback(
async ({ closeConfirmationModal, setConfirming }) => {
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()
setConfirming(false)
closeConfirmationModal()
const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular
const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular
if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:updatedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:updatedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
}
router.replace(
qs.stringify(
{
...parseSearchParams(searchParams),
page: selectAll ? '1' : undefined,
},
{ addQueryPrefix: true },
),
)
clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this
return null
}
router.replace(
qs.stringify(
{
...parseSearchParams(searchParams),
page: selectAll ? '1' : undefined,
},
{ addQueryPrefix: true },
),
)
clearRouteCache() // Use clearRouteCache instead of router.refresh, as we only need to clear the cache if the user has route caching enabled - clearRouteCache checks for this
return null
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return false
} catch (_err) {
setConfirming(false)
closeConfirmationModal()
return addDefaultError()
}
if (json.errors) {
json.errors.forEach((error) => toast.error(error.message))
} else {
addDefaultError()
}
return false
} catch (_err) {
return addDefaultError()
}
})
}, [
serverURL,
api,
slug,
getQueryParams,
i18n,
plural,
singular,
t,
router,
searchParams,
selectAll,
clearRouteCache,
addDefaultError,
])
})
},
[
serverURL,
api,
slug,
getQueryParams,
i18n,
plural,
singular,
t,
router,
searchParams,
selectAll,
clearRouteCache,
addDefaultError,
],
)
if (!versions?.drafts || selectAll === SelectAllStatus.None || !hasPermission) {
return null

View File

@@ -24,7 +24,7 @@ export { useUseTitleField } from '../../hooks/useUseAsTitle.js'
// elements
export { ConfirmationModal } from '../../elements/ConfirmationModal/index.js'
export type { OnCancel } from '../../elements/ConfirmationModal/index.js'
export type { OnCancel, OnConfirm } from '../../elements/ConfirmationModal/index.js'
export { Link } from '../../elements/Link/index.js'
export { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
export { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'

View File

@@ -38,8 +38,6 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
validate,
} = props
const pickerAppearance = datePickerProps?.pickerAppearance || 'default'
// Get the user timezone so we can adjust the displayed value against it
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
@@ -61,7 +59,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
setValue,
showError,
value,
} = useField<string>({
} = useField<Date>({
path,
validate: memoizedValidate,
})
@@ -69,11 +67,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
const timezonePath = path + '_tz'
const timezoneField = useFormFields(([fields, _]) => fields?.[timezonePath])
const supportedTimezones = config.admin.timezones.supportedTimezones
/**
* Date appearance doesn't include timestamps,
* which means we need to pin the time to always 12:00 for the selected date
*/
const isDateOnly = ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)
const selectedTimezone = timezoneField?.value as string
// The displayed value should be the original value, adjusted to the user's timezone
@@ -105,28 +99,15 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
if (!readOnly) {
if (timezone && selectedTimezone && incomingDate) {
// Create TZDate instances for the selected timezone
const TZDateWithSelectedTz = TZDate.tz(selectedTimezone)
const tzDateWithUTC = TZDate.tz(selectedTimezone)
if (isDateOnly) {
// We need to offset this hardcoded hour offset from the DatePicker elemenent
// this can be removed in 4.0 when we remove the hardcoded offset as it is a breaking change
// const tzOffset = incomingDate.getTimezoneOffset() / 60
const incomingOffset = incomingDate.getTimezoneOffset() / 60
const originalHour = incomingDate.getHours() + incomingOffset
incomingDate.setHours(originalHour)
// Creates a TZDate instance for the user's timezone — this is default behaviour of TZDate as it wraps the Date constructor
const dateToUserTz = new TZDate(incomingDate)
// Convert the original date as picked into the desired timezone.
const dateToSelectedTz = transpose(incomingDate, TZDateWithSelectedTz)
// Transpose the date to the selected timezone
const dateWithTimezone = transpose(dateToUserTz, tzDateWithUTC)
setValue(dateToSelectedTz.toISOString() || null)
} else {
// Creates a TZDate instance for the user's timezone — this is default behaviour of TZDate as it wraps the Date constructor
const dateToUserTz = new TZDate(incomingDate)
// Transpose the date to the selected timezone
const dateWithTimezone = transpose(dateToUserTz, TZDateWithSelectedTz)
setValue(dateWithTimezone.toISOString() || null)
}
setValue(dateWithTimezone.toISOString() || null)
} else {
setValue(incomingDate?.toISOString() || null)
}
@@ -192,6 +173,7 @@ const DateTimeFieldComponent: DateFieldClientComponent = (props) => {
selectedTimezone={selectedTimezone}
/>
)}
{AfterInput}
</div>
<RenderCustomComponent

View File

@@ -6,8 +6,7 @@
@extend %h4;
display: flex;
padding-bottom: base(1);
margin: 0;
margin-inline-end: $baseline;
margin: 0 $baseline 0 0;
cursor: pointer;
opacity: 0.5;
position: relative;

View File

@@ -43,6 +43,7 @@ export const Auth: React.FC<Props> = (props) => {
const modified = useFormModified()
const { i18n, t } = useTranslation()
const { docPermissions, isEditing, isInitializing } = useDocumentInfo()
const {
config: {
routes: { api },
@@ -50,11 +51,15 @@ export const Auth: React.FC<Props> = (props) => {
},
} = useConfig()
const enableFields =
!disableLocalStrategy ||
(typeof disableLocalStrategy === 'object' && disableLocalStrategy.enableFields === true)
const hasPermissionToUnlock: boolean = useMemo(() => {
const collection = permissions?.collections?.[collectionSlug]
const disabled = readOnly || isInitializing
if (collection) {
return Boolean('unlock' in collection ? collection.unlock : undefined)
}
return false
}, [permissions, collectionSlug])
const apiKeyPermissions =
docPermissions?.fields === true ? true : docPermissions?.fields?.enableAPIKey
@@ -69,16 +74,6 @@ export const Auth: React.FC<Props> = (props) => {
const canReadApiKey = apiKeyPermissions === true || apiKeyPermissions?.read
const hasPermissionToUnlock: boolean = useMemo(() => {
const collection = permissions?.collections?.[collectionSlug]
if (collection) {
return Boolean('unlock' in collection ? collection.unlock : undefined)
}
return false
}, [permissions, collectionSlug])
const handleChangePassword = useCallback(
(showPasswordFields: boolean) => {
if (showPasswordFields) {
@@ -134,13 +129,15 @@ export const Auth: React.FC<Props> = (props) => {
}
}, [modified])
if (disableLocalStrategy && !enableFields && !useAPIKey) {
if (disableLocalStrategy && !useAPIKey) {
return null
}
const disabled = readOnly || isInitializing
return (
<div className={[baseClass, className].filter(Boolean).join(' ')}>
{enableFields && (
{!disableLocalStrategy && (
<React.Fragment>
<EmailAndUsernameFields
loginWithUsername={loginWithUsername}
@@ -149,7 +146,7 @@ export const Auth: React.FC<Props> = (props) => {
readOnly={readOnly}
t={t}
/>
{(changingPassword || requirePassword) && (!disableLocalStrategy || !enableFields) && (
{(changingPassword || requirePassword) && (
<div className={`${baseClass}__changing-password`}>
<PasswordField
autoComplete="new-password"
@@ -178,7 +175,7 @@ export const Auth: React.FC<Props> = (props) => {
{t('general:cancel')}
</Button>
)}
{!changingPassword && !requirePassword && !disableLocalStrategy && (
{!changingPassword && !requirePassword && (
<Button
buttonStyle="secondary"
disabled={disabled}

53
pnpm-lock.yaml generated
View File

@@ -716,6 +716,9 @@ importers:
sass:
specifier: 1.77.4
version: 1.77.4
sonner:
specifier: ^1.7.0
version: 1.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
uuid:
specifier: 10.0.0
version: 10.0.0
@@ -3655,79 +3658,67 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@@ -4043,49 +4034,42 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-arm64-musl@1.0.1':
resolution: {integrity: sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@napi-rs/nice-linux-ppc64-gnu@1.0.1':
resolution: {integrity: sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==}
engines: {node: '>= 10'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-riscv64-gnu@1.0.1':
resolution: {integrity: sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-s390x-gnu@1.0.1':
resolution: {integrity: sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==}
engines: {node: '>= 10'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-x64-gnu@1.0.1':
resolution: {integrity: sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@napi-rs/nice-linux-x64-musl@1.0.1':
resolution: {integrity: sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@napi-rs/nice-win32-arm64-msvc@1.0.1':
resolution: {integrity: sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==}
@@ -4174,84 +4158,72 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-gnu@15.1.3':
resolution: {integrity: sha512-YbdaYjyHa4fPK4GR4k2XgXV0p8vbU1SZh7vv6El4bl9N+ZSiMfbmqCuCuNU1Z4ebJMumafaz6UCC2zaJCsdzjw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-gnu@15.1.5':
resolution: {integrity: sha512-rDJC4ctlYbK27tCyFUhgIv8o7miHNlpCjb2XXfTLQszwAUOSbcMN9q2y3urSrrRCyGVOd9ZR9a4S45dRh6JF3A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.0.3':
resolution: {integrity: sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-arm64-musl@15.1.3':
resolution: {integrity: sha512-qgH/aRj2xcr4BouwKG3XdqNu33SDadqbkqB6KaZZkozar857upxKakbRllpqZgWl/NDeSCBYPmUAZPBHZpbA0w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-arm64-musl@15.1.5':
resolution: {integrity: sha512-FG5RApf4Gu+J+pHUQxXPM81oORZrKBYKUaBTylEIQ6Lz17hKVDsLbSXInfXM0giclvXbyiLXjTv42sQMATmZ0A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.0.3':
resolution: {integrity: sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-gnu@15.1.3':
resolution: {integrity: sha512-uzafnTFwZCPN499fNVnS2xFME8WLC9y7PLRs/yqz5lz1X/ySoxfaK2Hbz74zYUdEg+iDZPd8KlsWaw9HKkLEVw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-gnu@15.1.5':
resolution: {integrity: sha512-NX2Ar3BCquAOYpnoYNcKz14eH03XuF7SmSlPzTSSU4PJe7+gelAjxo3Y7F2m8+hLT8ZkkqElawBp7SWBdzwqQw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.0.3':
resolution: {integrity: sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-musl@15.1.3':
resolution: {integrity: sha512-el6GUFi4SiDYnMTTlJJFMU+GHvw0UIFnffP1qhurrN1qJV3BqaSRUjkDUgVV44T6zpw1Lc6u+yn0puDKHs+Sbw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-linux-x64-musl@15.1.5':
resolution: {integrity: sha512-EQgqMiNu3mrV5eQHOIgeuh6GB5UU57tu17iFnLfBEhYfiOfyK+vleYKh2dkRVkV6ayx3eSqbIYgE7J7na4hhcA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.0.3':
resolution: {integrity: sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==}
@@ -4538,25 +4510,21 @@ packages:
resolution: {integrity: sha512-otVbS4zeo3n71zgGLBYRTriDzc0zpruC0WI3ICwjpIk454cLwGV0yzh4jlGYWQJYJk0BRAmXFd3ooKIF+bKBHw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-arm64-musl@1.12.0':
resolution: {integrity: sha512-IStQDjIT7Lzmqg1i9wXvPL/NsYsxF24WqaQFS8b8rxra+z0VG7saBOsEnOaa4jcEY8MVpLYabFhTV+fSsA2vnA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-linux-x64-gnu@1.12.0':
resolution: {integrity: sha512-SipT7EVORz8pOQSFwemOm91TpSiBAGmOjG830/o+aLEsvQ4pEy223+SAnCfITh7+AahldYsJnVoIs519jmIlKQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-musl@1.12.0':
resolution: {integrity: sha512-mGh0XfUzKdn+WFaqPacziNraCWL5znkHRfQVxG9avGS9zb2KC/N1EBbPzFqutDwixGDP54r2gx4q54YCJEZ4iQ==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-wasm32-wasi@1.12.0':
resolution: {integrity: sha512-SZN6v7apKmQf/Vwiqb6e/s3Y2Oacw8uW8V2i1AlxtyaEFvnFE0UBn89zq6swEwE3OCajNWs0yPvgAXUMddYc7Q==}
@@ -5044,28 +5012,24 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@swc/core-linux-arm64-musl@1.10.12':
resolution: {integrity: sha512-oqhSmV+XauSf0C//MoQnVErNUB/5OzmSiUzuazyLsD5pwqKNN+leC3JtRQ/QVzaCpr65jv9bKexT9+I2Tt3xDw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@swc/core-linux-x64-gnu@1.10.12':
resolution: {integrity: sha512-XldSIHyjD7m1Gh+/8rxV3Ok711ENLI420CU2EGEqSe3VSGZ7pHJvJn9ZFbYpWhsLxPqBYMFjp3Qw+J6OXCPXCA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@swc/core-linux-x64-musl@1.10.12':
resolution: {integrity: sha512-wvPXzJxzPgTqhyp1UskOx1hRTtdWxlyFD1cGWOxgLsMik0V9xKRgqKnMPv16Nk7L9xl6quQ6DuUHj9ID7L3oVw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@swc/core-win32-arm64-msvc@1.10.12':
resolution: {integrity: sha512-TUYzWuu1O7uyIcRfxdm6Wh1u+gNnrW5M1DUgDOGZLsyQzgc2Zjwfh2llLhuAIilvCVg5QiGbJlpibRYJ/8QGsg==}
@@ -9382,6 +9346,12 @@ packages:
sonic-boom@4.2.0:
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
sonner@1.7.0:
resolution: {integrity: sha512-W6dH7m5MujEPyug3lpI2l3TC3Pp1+LTgK0Efg+IHDrBbtEjyCmCHHo6yfNBOsf1tFZ6zf+jceWwB38baC8yO9g==}
peerDependencies:
react: 19.0.0
react-dom: 19.0.0
sonner@1.7.2:
resolution: {integrity: sha512-zMbseqjrOzQD1a93lxahm+qMGxWovdMxBlkTbbnZdNqVLt4j+amF9PQxUCL32WfztOFt9t9ADYkejAL3jF9iNA==}
peerDependencies:
@@ -19347,6 +19317,11 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
sonner@1.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
sonner@1.7.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0

View File

@@ -56,7 +56,7 @@ export const ArchiveBlock: React.FC<
<div className="my-16" id={`block-${id}`}>
{introContent && (
<div className="container mb-16">
<RichText className="ms-0 max-w-[48rem]" data={introContent} enableGutter={false} />
<RichText className="ml-0 max-w-[48rem]" data={introContent} enableGutter={false} />
</div>
)}
<CollectionArchive posts={posts} />

View File

@@ -56,7 +56,7 @@ export const ArchiveBlock: React.FC<
<div className="my-16" id={`block-${id}`}>
{introContent && (
<div className="container mb-16">
<RichText className="ms-0 max-w-[48rem]" data={introContent} enableGutter={false} />
<RichText className="ml-0 max-w-[48rem]" data={introContent} enableGutter={false} />
</div>
)}
<CollectionArchive posts={posts} />

View File

@@ -22,13 +22,14 @@ import {
hiddenAccessSlug,
hiddenFieldsSlug,
nonAdminEmail,
nonAdminUserEmail,
nonAdminUserSlug,
publicUserEmail,
publicUsersSlug,
readNotUpdateGlobalSlug,
readOnlyGlobalSlug,
readOnlySlug,
relyOnRequestHeadersSlug,
restrictedVersionsAdminPanelSlug,
restrictedVersionsSlug,
secondArrayText,
siblingDataSlug,
@@ -323,35 +324,6 @@ export default buildConfigWithDefaults(
],
versions: true,
},
{
slug: restrictedVersionsAdminPanelSlug,
access: {
read: ({ req: { user } }) => {
if (user) {
return true
}
return false
},
readVersions: () => {
return {
'version.hidden': {
not_equals: true,
},
}
},
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'hidden',
type: 'checkbox',
},
],
versions: true,
},
{
slug: siblingDataSlug,
access: openAccess,

View File

@@ -35,7 +35,6 @@ import {
readNotUpdateGlobalSlug,
readOnlyGlobalSlug,
readOnlySlug,
restrictedVersionsAdminPanelSlug,
restrictedVersionsSlug,
slug,
unrestrictedSlug,
@@ -64,7 +63,6 @@ describe('Access Control', () => {
let richTextUrl: AdminUrlUtil
let readOnlyGlobalUrl: AdminUrlUtil
let restrictedVersionsUrl: AdminUrlUtil
let restrictedVersionsAdminPanelUrl: AdminUrlUtil
let userRestrictedCollectionURL: AdminUrlUtil
let userRestrictedGlobalURL: AdminUrlUtil
let disabledFields: AdminUrlUtil
@@ -83,7 +81,6 @@ describe('Access Control', () => {
readOnlyCollectionUrl = new AdminUrlUtil(serverURL, readOnlySlug)
readOnlyGlobalUrl = new AdminUrlUtil(serverURL, readOnlySlug)
restrictedVersionsUrl = new AdminUrlUtil(serverURL, restrictedVersionsSlug)
restrictedVersionsAdminPanelUrl = new AdminUrlUtil(serverURL, restrictedVersionsAdminPanelSlug)
userRestrictedCollectionURL = new AdminUrlUtil(serverURL, userRestrictedCollectionSlug)
userRestrictedGlobalURL = new AdminUrlUtil(serverURL, userRestrictedGlobalSlug)
disabledFields = new AdminUrlUtil(serverURL, disabledSlug)
@@ -560,26 +557,16 @@ describe('Access Control', () => {
beforeAll(async () => {
existingDoc = await payload.create({
collection: restrictedVersionsAdminPanelSlug,
collection: restrictedVersionsSlug,
data: {
name: 'name',
},
})
await payload.update({
collection: restrictedVersionsAdminPanelSlug,
id: existingDoc.id,
data: {
hidden: true,
},
})
})
test('versions tab should not show', async () => {
await page.goto(restrictedVersionsAdminPanelUrl.edit(existingDoc.id))
await page.locator('.doc-tabs__tabs').getByLabel('Versions').click()
const rows = page.locator('.versions table tbody tr')
await expect(rows).toHaveCount(1)
test('versions sidebar should not show', async () => {
await page.goto(restrictedVersionsUrl.edit(existingDoc.id))
await expect(page.locator('.versions-count')).toBeHidden()
})
})

View File

@@ -6,11 +6,66 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* Supported timezones in IANA format.
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "supportedTimezones".
*/
export type SupportedTimezones =
| 'Pacific/Midway'
| 'Pacific/Niue'
| 'Pacific/Honolulu'
| 'Pacific/Rarotonga'
| 'America/Anchorage'
| 'Pacific/Gambier'
| 'America/Los_Angeles'
| 'America/Tijuana'
| 'America/Denver'
| 'America/Phoenix'
| 'America/Chicago'
| 'America/Guatemala'
| 'America/New_York'
| 'America/Bogota'
| 'America/Caracas'
| 'America/Santiago'
| 'America/Buenos_Aires'
| 'America/Sao_Paulo'
| 'Atlantic/South_Georgia'
| 'Atlantic/Azores'
| 'Atlantic/Cape_Verde'
| 'Europe/London'
| 'Europe/Berlin'
| 'Africa/Lagos'
| 'Europe/Athens'
| 'Africa/Cairo'
| 'Europe/Moscow'
| 'Asia/Riyadh'
| 'Asia/Dubai'
| 'Asia/Baku'
| 'Asia/Karachi'
| 'Asia/Tashkent'
| 'Asia/Calcutta'
| 'Asia/Dhaka'
| 'Asia/Almaty'
| 'Asia/Jakarta'
| 'Asia/Bangkok'
| 'Asia/Shanghai'
| 'Asia/Singapore'
| 'Asia/Tokyo'
| 'Asia/Seoul'
| 'Australia/Sydney'
| 'Pacific/Guam'
| 'Pacific/Noumea'
| 'Pacific/Auckland'
| 'Pacific/Fiji';
export interface Config {
auth: {
users: UserAuthOperations;
'public-users': PublicUserAuthOperations;
};
blocks: {};
collections: {
users: User;
'public-users': PublicUser;
@@ -22,7 +77,6 @@ export interface Config {
'user-restricted-collection': UserRestrictedCollection;
'create-not-update-collection': CreateNotUpdateCollection;
'restricted-versions': RestrictedVersion;
'restricted-versions-admin-panel': RestrictedVersionsAdminPanel;
'sibling-data': SiblingDatum;
'rely-on-request-headers': RelyOnRequestHeader;
'doc-level-access': DocLevelAccess;
@@ -50,7 +104,6 @@ export interface Config {
'user-restricted-collection': UserRestrictedCollectionSelect<false> | UserRestrictedCollectionSelect<true>;
'create-not-update-collection': CreateNotUpdateCollectionSelect<false> | CreateNotUpdateCollectionSelect<true>;
'restricted-versions': RestrictedVersionsSelect<false> | RestrictedVersionsSelect<true>;
'restricted-versions-admin-panel': RestrictedVersionsAdminPanelSelect<false> | RestrictedVersionsAdminPanelSelect<true>;
'sibling-data': SiblingDataSelect<false> | SiblingDataSelect<true>;
'rely-on-request-headers': RelyOnRequestHeadersSelect<false> | RelyOnRequestHeadersSelect<true>;
'doc-level-access': DocLevelAccessSelect<false> | DocLevelAccessSelect<true>;
@@ -256,17 +309,6 @@ export interface RestrictedVersion {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted-versions-admin-panel".
*/
export interface RestrictedVersionsAdminPanel {
id: string;
name?: string | null;
hidden?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "sibling-data".
@@ -672,10 +714,6 @@ export interface PayloadLockedDocument {
relationTo: 'restricted-versions';
value: string | RestrictedVersion;
} | null)
| ({
relationTo: 'restricted-versions-admin-panel';
value: string | RestrictedVersionsAdminPanel;
} | null)
| ({
relationTo: 'sibling-data';
value: string | SiblingDatum;
@@ -886,16 +924,6 @@ export interface RestrictedVersionsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted-versions-admin-panel_select".
*/
export interface RestrictedVersionsAdminPanelSelect<T extends boolean = true> {
name?: T;
hidden?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "sibling-data_select".

View File

@@ -12,7 +12,6 @@ export const createNotUpdateCollectionSlug = 'create-not-update-collection'
export const userRestrictedGlobalSlug = 'user-restricted-global'
export const readNotUpdateGlobalSlug = 'read-not-update-global'
export const restrictedVersionsSlug = 'restricted-versions'
export const restrictedVersionsAdminPanelSlug = 'restricted-versions-admin-panel'
export const siblingDataSlug = 'sibling-data'
export const relyOnRequestHeadersSlug = 'rely-on-request-headers'
export const docLevelAccessSlug = 'doc-level-access'

View File

@@ -9,7 +9,7 @@ import { devUser } from '../credentials.js'
import {
apiKeysSlug,
namedSaveToJWTValue,
partialDisableLocalStrategiesSlug,
partialDisableLocaleStrategiesSlug,
publicUsersSlug,
saveToJWTKey,
slug,
@@ -185,7 +185,7 @@ export default buildConfigWithDefaults({
],
},
{
slug: partialDisableLocalStrategiesSlug,
slug: partialDisableLocaleStrategiesSlug,
auth: {
disableLocalStrategy: {
// optionalPassword: true,

View File

@@ -12,7 +12,7 @@ import { initPayloadInt } from '../helpers/initPayloadInt.js'
import {
apiKeysSlug,
namedSaveToJWTValue,
partialDisableLocalStrategiesSlug,
partialDisableLocaleStrategiesSlug,
publicUsersSlug,
saveToJWTKey,
slug,
@@ -720,7 +720,7 @@ describe('Auth', () => {
it('should allow create of a user with disableLocalStrategy', async () => {
const email = 'test@example.com'
const user = await payload.create({
collection: partialDisableLocalStrategiesSlug,
collection: partialDisableLocaleStrategiesSlug,
data: {
email,
// password is not required
@@ -730,7 +730,7 @@ describe('Auth', () => {
})
it('should retain fields when auth.disableLocalStrategy.enableFields is true', () => {
const authFields = payload.collections[partialDisableLocalStrategiesSlug].config.fields
const authFields = payload.collections[partialDisableLocaleStrategiesSlug].config.fields
// eslint-disable-next-line jest/no-conditional-in-test
.filter((field) => 'name' in field && field.name)
.map((field) => (field as FieldAffectingData).name)
@@ -750,7 +750,7 @@ describe('Auth', () => {
it('should prevent login of user with disableLocalStrategy.', async () => {
await payload.create({
collection: partialDisableLocalStrategiesSlug,
collection: partialDisableLocaleStrategiesSlug,
data: {
email: devUser.email,
password: devUser.password,
@@ -759,7 +759,7 @@ describe('Auth', () => {
await expect(async () => {
await payload.login({
collection: partialDisableLocalStrategiesSlug,
collection: partialDisableLocaleStrategiesSlug,
data: {
email: devUser.email,
password: devUser.password,
@@ -769,7 +769,7 @@ describe('Auth', () => {
})
it('rest - should prevent login', async () => {
const response = await restClient.POST(`/${partialDisableLocalStrategiesSlug}/login`, {
const response = await restClient.POST(`/${partialDisableLocaleStrategiesSlug}/login`, {
body: JSON.stringify({
email,
password,

View File

@@ -63,14 +63,14 @@ export type SupportedTimezones =
export interface Config {
auth: {
users: UserAuthOperations;
'partial-disable-local-strategies': PartialDisableLocalStrategyAuthOperations;
'partial-disable-locale-strategies': PartialDisableLocaleStrategyAuthOperations;
'api-keys': ApiKeyAuthOperations;
'public-users': PublicUserAuthOperations;
};
blocks: {};
collections: {
users: User;
'partial-disable-local-strategies': PartialDisableLocalStrategy;
'partial-disable-locale-strategies': PartialDisableLocaleStrategy;
'api-keys': ApiKey;
'public-users': PublicUser;
relationsCollection: RelationsCollection;
@@ -81,7 +81,7 @@ export interface Config {
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
'partial-disable-local-strategies': PartialDisableLocalStrategiesSelect<false> | PartialDisableLocalStrategiesSelect<true>;
'partial-disable-locale-strategies': PartialDisableLocaleStrategiesSelect<false> | PartialDisableLocaleStrategiesSelect<true>;
'api-keys': ApiKeysSelect<false> | ApiKeysSelect<true>;
'public-users': PublicUsersSelect<false> | PublicUsersSelect<true>;
relationsCollection: RelationsCollectionSelect<false> | RelationsCollectionSelect<true>;
@@ -99,8 +99,8 @@ export interface Config {
| (User & {
collection: 'users';
})
| (PartialDisableLocalStrategy & {
collection: 'partial-disable-local-strategies';
| (PartialDisableLocaleStrategy & {
collection: 'partial-disable-locale-strategies';
})
| (ApiKey & {
collection: 'api-keys';
@@ -131,7 +131,7 @@ export interface UserAuthOperations {
password: string;
};
}
export interface PartialDisableLocalStrategyAuthOperations {
export interface PartialDisableLocaleStrategyAuthOperations {
forgotPassword: {
email: string;
password: string;
@@ -227,9 +227,9 @@ export interface User {
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "partial-disable-local-strategies".
* via the `definition` "partial-disable-locale-strategies".
*/
export interface PartialDisableLocalStrategy {
export interface PartialDisableLocaleStrategy {
id: string;
updatedAt: string;
createdAt: string;
@@ -296,8 +296,8 @@ export interface PayloadLockedDocument {
value: string | User;
} | null)
| ({
relationTo: 'partial-disable-local-strategies';
value: string | PartialDisableLocalStrategy;
relationTo: 'partial-disable-locale-strategies';
value: string | PartialDisableLocaleStrategy;
} | null)
| ({
relationTo: 'api-keys';
@@ -318,8 +318,8 @@ export interface PayloadLockedDocument {
value: string | User;
}
| {
relationTo: 'partial-disable-local-strategies';
value: string | PartialDisableLocalStrategy;
relationTo: 'partial-disable-locale-strategies';
value: string | PartialDisableLocaleStrategy;
}
| {
relationTo: 'api-keys';
@@ -344,8 +344,8 @@ export interface PayloadPreference {
value: string | User;
}
| {
relationTo: 'partial-disable-local-strategies';
value: string | PartialDisableLocalStrategy;
relationTo: 'partial-disable-locale-strategies';
value: string | PartialDisableLocaleStrategy;
}
| {
relationTo: 'api-keys';
@@ -427,9 +427,9 @@ export interface UsersSelect<T extends boolean = true> {
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "partial-disable-local-strategies_select".
* via the `definition` "partial-disable-locale-strategies_select".
*/
export interface PartialDisableLocalStrategiesSelect<T extends boolean = true> {
export interface PartialDisableLocaleStrategiesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;

View File

@@ -4,7 +4,7 @@ export const publicUsersSlug = 'public-users'
export const apiKeysSlug = 'api-keys'
export const partialDisableLocalStrategiesSlug = 'partial-disable-local-strategies'
export const partialDisableLocaleStrategiesSlug = 'partial-disable-locale-strategies'
export const namedSaveToJWTValue = 'namedSaveToJWT value'

View File

@@ -28,8 +28,6 @@ const dirname = path.resolve(currentFolder, '../../')
const { beforeAll, beforeEach, describe } = test
const londonTimezone = 'Europe/London'
const aucklandTimezone = 'Pacific/Auckland'
const detroitTimezone = 'America/Detroit'
let payload: PayloadTestSDK<Config>
let client: RESTClient
@@ -254,6 +252,9 @@ describe('Date', () => {
*
* See: https://github.com/microsoft/playwright/issues/27138
*/
test.use({
timezoneId: londonTimezone,
})
test('should display the value in the selected time', async () => {
const {
@@ -495,851 +496,125 @@ describe('Date', () => {
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
})
})
describe('Date with TZ - Context: America/Detroit', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))
url = new AdminUrlUtil(serverURL, dateFieldsSlug)
const context = await browser.newContext({ timezoneId: detroitTimezone })
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient({ defaultSlug: 'users', serverURL })
await client.login()
await ensureCompilationIsDone({ page, serverURL })
})
test('displayed value should remain unchanged', async () => {
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
})
await page.goto(url.edit(existingDoc!.id))
const result = await page.evaluate(() => {
return Intl.DateTimeFormat().resolvedOptions().timeZone
})
await expect(() => {
// Confirm that the emulated timezone is set to London
expect(result).toEqual(detroitTimezone)
}).toPass({ timeout: 10000, intervals: [100] })
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const expectedDateOnlyValue = '08/12/2027'
const expectedDateTimeValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time
await expect(dateOnlyLocator).toHaveValue(expectedDateOnlyValue)
await expect(dateTimeLocator).toHaveValue(expectedDateTimeValue)
})
test('creates the expected UTC value when the selected timezone is Paris - no daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jan 1, 2025 6:00 PM'
// We're testing this specific date because Paris has no daylight savings time in January
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-01-01T17:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
test('creates the expected UTC value when the selected timezone is Paris - with daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jul 1, 2025 6:00 PM'
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-07-01T16:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
test('creates the expected UTC value when the selected timezone is Auckland - no daylight savings', async () => {
// We send this value through the input
const expectedDateTimeInput = 'Jan 1, 2025 6:00 PM'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyInput = '01/02/2025' // 2nd July 2025
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedDateTimeUTCValue = '2025-01-01T05:00:00.000Z'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyUTCValue = '2025-01-01T23:00:00.000Z' // 2nd July 2025 at 12PM in Auckland
await page.goto(url.create)
// Default date field - filling it because it's required for the form to be valid
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
// Date input fields
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
// Fill in date only
const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control`
const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dateOnlyDropdownSelector)
await page.click(dateOnlytimezoneSelector)
await dateOnlyLocator.fill(expectedDateOnlyInput)
// Fill in date and time
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateTimeInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue)
expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue)
})
test('creates the expected UTC value when the selected timezone is Auckland - with daylight savings', async () => {
// We send this value through the input
const expectedDateTimeInput = 'Jul 1, 2025 6:00 PM'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyInput = '07/02/2025' // 2nd July 2025
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedDateTimeUTCValue = '2025-07-01T06:00:00.000Z'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyUTCValue = '2025-07-02T00:00:00.000Z' // 2nd July 2025 at 12PM in Auckland
await page.goto(url.create)
// Default date field - filling it because it's required for the form to be valid
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
// Date input fields
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
// Fill in date only
const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control`
const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dateOnlyDropdownSelector)
await page.click(dateOnlytimezoneSelector)
await dateOnlyLocator.fill(expectedDateOnlyInput)
// Fill in date and time
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateTimeInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue)
expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue)
})
})
describe('Date with TZ - Context: Europe/London', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))
url = new AdminUrlUtil(serverURL, dateFieldsSlug)
const context = await browser.newContext({ timezoneId: londonTimezone })
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient({ defaultSlug: 'users', serverURL })
await client.login()
await ensureCompilationIsDone({ page, serverURL })
})
test('displayed value should remain unchanged', async () => {
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
})
await page.goto(url.edit(existingDoc!.id))
const result = await page.evaluate(() => {
return Intl.DateTimeFormat().resolvedOptions().timeZone
})
await expect(() => {
// Confirm that the emulated timezone is set to London
expect(result).toEqual(londonTimezone)
}).toPass({ timeout: 10000, intervals: [100] })
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const expectedDateOnlyValue = '08/12/2027'
const expectedDateTimeValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time
await expect(dateOnlyLocator).toHaveValue(expectedDateOnlyValue)
await expect(dateTimeLocator).toHaveValue(expectedDateTimeValue)
})
test('creates the expected UTC value when the selected timezone is Paris - no daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jan 1, 2025 6:00 PM'
// We're testing this specific date because Paris has no daylight savings time in January
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-01-01T17:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
test('creates the expected UTC value when the selected timezone is Paris - with daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jul 1, 2025 6:00 PM'
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-07-01T16:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
test('creates the expected UTC value when the selected timezone is Auckland - no daylight savings', async () => {
// We send this value through the input
const expectedDateTimeInput = 'Jan 1, 2025 6:00 PM'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyInput = '01/02/2025' // 2nd July 2025
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedDateTimeUTCValue = '2025-01-01T05:00:00.000Z'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyUTCValue = '2025-01-01T23:00:00.000Z' // 2nd July 2025 at 12PM in Auckland
await page.goto(url.create)
// Default date field - filling it because it's required for the form to be valid
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
// Date input fields
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
// Fill in date only
const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control`
const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dateOnlyDropdownSelector)
await page.click(dateOnlytimezoneSelector)
await dateOnlyLocator.fill(expectedDateOnlyInput)
// Fill in date and time
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateTimeInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue)
expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue)
})
test('creates the expected UTC value when the selected timezone is Auckland - with daylight savings', async () => {
// We send this value through the input
const expectedDateTimeInput = 'Jul 1, 2025 6:00 PM'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyInput = '07/02/2025' // 2nd July 2025
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedDateTimeUTCValue = '2025-07-01T06:00:00.000Z'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyUTCValue = '2025-07-02T00:00:00.000Z' // 2nd July 2025 at 12PM in Auckland
await page.goto(url.create)
// Default date field - filling it because it's required for the form to be valid
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
// Date input fields
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
// Fill in date only
const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control`
const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dateOnlyDropdownSelector)
await page.click(dateOnlytimezoneSelector)
await dateOnlyLocator.fill(expectedDateOnlyInput)
// Fill in date and time
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateTimeInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue)
expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue)
})
})
describe('Date with TZ - Context: Pacific/Auckland', () => {
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({
dirname,
// prebuild,
}))
url = new AdminUrlUtil(serverURL, dateFieldsSlug)
const context = await browser.newContext({ timezoneId: aucklandTimezone })
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({ page, serverURL })
})
beforeEach(async () => {
await reInitializeDB({
serverURL,
snapshotKey: 'fieldsTest',
uploadsDir: path.resolve(dirname, './collections/Upload/uploads'),
})
if (client) {
await client.logout()
}
client = new RESTClient({ defaultSlug: 'users', serverURL })
await client.login()
await ensureCompilationIsDone({ page, serverURL })
})
test('displayed value should remain unchanged', async () => {
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
})
await page.goto(url.edit(existingDoc!.id))
const result = await page.evaluate(() => {
return Intl.DateTimeFormat().resolvedOptions().timeZone
})
await expect(() => {
// Confirm that the emulated timezone is set to London
expect(result).toEqual(aucklandTimezone)
}).toPass({ timeout: 10000, intervals: [100] })
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const expectedDateOnlyValue = '08/12/2027'
const expectedDateTimeValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time
await expect(dateOnlyLocator).toHaveValue(expectedDateOnlyValue)
await expect(dateTimeLocator).toHaveValue(expectedDateTimeValue)
})
test('creates the expected UTC value when the selected timezone is Paris - no daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jan 1, 2025 6:00 PM'
// We're testing this specific date because Paris has no daylight savings time in January
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-01-01T17:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
test('creates the expected UTC value when the selected timezone is Paris - with daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jul 1, 2025 6:00 PM'
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-07-01T16:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
test('creates the expected UTC value when the selected timezone is Auckland - no daylight savings', async () => {
// We send this value through the input
const expectedDateTimeInput = 'Jan 1, 2025 6:00 PM'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyInput = '01/02/2025' // 2nd July 2025
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedDateTimeUTCValue = '2025-01-01T05:00:00.000Z'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyUTCValue = '2025-01-01T23:00:00.000Z' // 2nd July 2025 at 12PM in Auckland
await page.goto(url.create)
// Default date field - filling it because it's required for the form to be valid
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
// Date input fields
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
// Fill in date only
const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control`
const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dateOnlyDropdownSelector)
await page.click(dateOnlytimezoneSelector)
await dateOnlyLocator.fill(expectedDateOnlyInput)
// Fill in date and time
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateTimeInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue)
expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue)
})
test('creates the expected UTC value when the selected timezone is Auckland - with daylight savings', async () => {
// We send this value through the input
const expectedDateTimeInput = 'Jul 1, 2025 6:00 PM'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyInput = '07/02/2025' // 2nd July 2025
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedDateTimeUTCValue = '2025-07-01T06:00:00.000Z'
// The timestamp for this date should be normalised to 12PM local time
const expectedDateOnlyUTCValue = '2025-07-02T00:00:00.000Z' // 2nd July 2025 at 12PM in Auckland
await page.goto(url.create)
// Default date field - filling it because it's required for the form to be valid
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
// Date input fields
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dateOnlyLocator = page.locator(
'#field-defaultWithTimezone .react-datepicker-wrapper input',
)
// Fill in date only
const dateOnlyDropdownSelector = `#field-defaultWithTimezone .rs__control`
const dateOnlytimezoneSelector = `#field-defaultWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dateOnlyDropdownSelector)
await page.click(dateOnlytimezoneSelector)
await dateOnlyLocator.fill(expectedDateOnlyInput)
// Fill in date and time
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Auckland")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateTimeInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedDateTimeUTCValue)
expect(existingDoc?.defaultWithTimezone).toEqual(expectedDateOnlyUTCValue)
test('creates the expected UTC value when the timezone is Paris - no daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jan 1, 2025 6:00 PM'
// We're testing this specific date because Paris has no daylight savings time in January
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-01-01T17:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
test('creates the expected UTC value when the timezone is Paris - with daylight savings', async () => {
// We send this value through the input
const expectedDateInput = 'Jul 1, 2025 6:00 PM'
// We're testing specific date because Paris has daylight savings time in July (+1 hour to the local timezone)
// but the UTC date will be different from 6PM local time in the summer versus the winter
const expectedUTCValue = '2025-07-01T16:00:00.000Z'
await page.goto(url.create)
const dateField = page.locator('#field-default input')
await dateField.fill('01/01/2025')
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const dropdownControlSelector = `#field-dayAndTimeWithTimezone .rs__control`
const timezoneOptionSelector = `#field-dayAndTimeWithTimezone .rs__menu .rs__option:has-text("Paris")`
await page.click(dropdownControlSelector)
await page.click(timezoneOptionSelector)
await dateTimeLocator.fill(expectedDateInput)
await saveDocAndAssert(page)
const docID = page.url().split('/').pop()
// eslint-disable-next-line payload/no-flaky-assertions
expect(docID).toBeTruthy()
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
where: {
id: {
equals: docID,
},
},
})
// eslint-disable-next-line payload/no-flaky-assertions
expect(existingDoc?.dayAndTimeWithTimezone).toEqual(expectedUTCValue)
})
describe('while timezone is set to London', () => {
test('displayed value should be the same while timezone is set to London', async () => {
const {
docs: [existingDoc],
} = await payload.find({
collection: dateFieldsSlug,
})
await page.goto(url.edit(existingDoc!.id))
const result = await page.evaluate(() => {
return Intl.DateTimeFormat().resolvedOptions().timeZone
})
await expect(() => {
// Confirm that the emulated timezone is set to London
expect(result).toEqual(londonTimezone)
}).toPass({ timeout: 10000, intervals: [100] })
const dateTimeLocator = page.locator(
'#field-dayAndTimeWithTimezone .react-datepicker-wrapper input',
)
const expectedValue = 'Aug 12, 2027 10:00 AM' // This is the seeded value for 10AM at Asia/Tokyo time
await expect(dateTimeLocator).toHaveValue(expectedValue)
})
})
})
})

View File

@@ -22,38 +22,6 @@ export default buildConfigWithDefaults({
type: 'text',
name: 'title',
},
{
name: 'selectField',
type: 'select',
required: true,
interfaceName: 'MySelectOptions',
options: [
{
label: 'Option 1',
value: 'option-1',
},
{
label: 'Option 2',
value: 'option-2',
},
],
},
{
name: 'radioField',
type: 'radio',
required: true,
interfaceName: 'MyRadioOptions',
options: [
{
label: 'Option 1',
value: 'option-1',
},
{
label: 'Option 2',
value: 'option-2',
},
],
},
],
},
{

View File

@@ -6,16 +6,6 @@
* and re-run `payload generate:types` to regenerate this file.
*/
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "MySelectOptions".
*/
export type MySelectOptions = 'option-1' | 'option-2';
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "MyRadioOptions".
*/
export type MyRadioOptions = 'option-1' | 'option-2';
/**
* Supported timezones in IANA format.
*
@@ -142,8 +132,6 @@ export interface Post {
id: string;
text?: string | null;
title?: string | null;
selectField: MySelectOptions;
radioField: MyRadioOptions;
updatedAt: string;
createdAt: string;
}
@@ -261,8 +249,6 @@ export interface PayloadMigration {
export interface PostsSelect<T extends boolean = true> {
text?: T;
title?: T;
selectField?: T;
radioField?: T;
updatedAt?: T;
createdAt?: T;
}

View File

@@ -10,14 +10,7 @@ import type {
import payload from 'payload'
import { describe, expect, test } from 'tstyche'
import type {
Menu,
MyRadioOptions,
MySelectOptions,
Post,
SupportedTimezones,
User,
} from './payload-types.js'
import type { Menu, Post, User } from './payload-types.js'
const asType = <T>() => {
return '' as T
@@ -131,18 +124,4 @@ describe('Types testing', () => {
}>()
})
})
describe('generated types', () => {
test('has SupportedTimezones', () => {
expect<SupportedTimezones>().type.toBeAssignableTo<string>()
})
test('has global generated options interface based on select field', () => {
expect(asType<Post['selectField']>()).type.toBe<MySelectOptions>()
})
test('has global generated options interface based on radio field', () => {
expect(asType<Post['radioField']>()).type.toBe<MyRadioOptions>()
})
})
})

View File

@@ -743,49 +743,6 @@ describe('Versions', () => {
expect(versionsTabUpdated).toBeTruthy()
})
})
describe('Scheduled publish', () => {
beforeAll(() => {
url = new AdminUrlUtil(serverURL, draftCollectionSlug)
})
test('should schedule publish', async () => {
await page.goto(url.create)
await page.waitForURL(url.create)
await page.locator('#field-title').fill('scheduled publish')
await page.locator('#field-description').fill('scheduled publish description')
// schedule publish should not be available before document has been saved
await page.locator('#action-save-popup').click()
await expect(page.locator('#schedule-publish')).not.toBeVisible()
// save draft then try to schedule publish
await saveDocAndAssert(page)
await page.locator('#action-save-popup').click()
await page.locator('#schedule-publish').click()
// drawer should open
await expect(page.locator('.schedule-publish__drawer-header')).toBeVisible()
// nothing in scheduled
await expect(page.locator('.drawer__content')).toContainText('No upcoming events scheduled.')
// set date and time
await page.locator('.date-time-picker input').fill('Feb 21, 2050 12:00 AM')
await page.keyboard.press('Enter')
// save the scheduled publish
await page.locator('#scheduled-publish-save').click()
// delete the scheduled event after it was made
await page.locator('.cell-delete').locator('.btn').click()
// see toast deleted successfully
await expect(
page.locator('.payload-toast-item:has-text("Deleted successfully.")'),
).toBeVisible()
})
})
describe('Collections - publish specific locale', () => {
beforeAll(() => {
url = new AdminUrlUtil(serverURL, localizedCollectionSlug)

View File

@@ -31,7 +31,7 @@
}
],
"paths": {
"@payload-config": ["./test/versions/config.ts"],
"@payload-config": ["./test/plugin-search/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],