Compare commits

...

8 Commits

Author SHA1 Message Date
Paul Popus
3861e4820b fix: remove 'id' from the generated types for globals 2024-09-27 09:35:44 -06:00
Germán Jabloñski
17e0547db3 feat(payload, ui): add admin.allowEdit relationship field (#8398)
This PR adds a new property `allowEdit` to the admin of the relationship
field. It is very similar to the existing `allowCreate`, only in this
case it hides the edit icon:

<img width="796" alt="image"
src="https://github.com/user-attachments/assets/bbe79bb2-db06-4ec4-b023-2f1c53330fcb">
2024-09-27 09:22:03 -04:00
Elliot DeNolf
e900e8974b chore(release): v3.0.0-beta.109 [skip ci] 2024-09-26 14:00:43 -04:00
Dan Ribbens
adc9bb5cbd fix(db-mongodb): docs duplicated in list view with drafts (#8435)
fixes #8430
2024-09-26 13:57:07 -04:00
Germán Jabloñski
a09811f5d4 fix(richtext-lexical): add max-width to tables (temporary fix for overflow). (#8431)
This is a half-baked and temporary solution to
https://github.com/payloadcms/payload/issues/8036

As I said there:
- ideally tables would have a horizontal scroll independent of the rest
of the document, just like Notion does.
- the solution in this PR can make the experience of resizing columns
frustrating

However, despite that drawback, it is arguably a better behavior than
the current one where they can have overflow over the editor container.

Until the ideal solution is implemented, let's default to this behavior.

## Before


![image](https://github.com/user-attachments/assets/b2856a3f-4b43-45f0-a7db-00c53fe5c980)


## After

![image](https://github.com/user-attachments/assets/2f60d186-d614-4c72-968c-137820812e11)
2024-09-26 14:37:25 -03:00
Paul
c73f6c74b3 fix(ui): autosave and preventLeaveWithoutSaving interfering with fetching form-state reliably (#8434)
Removes the setModified call from Autosave logic and updates the
`preventLeaveWithoutSaving` logic in Document info to actually disable
if autosave is enabled (previously it always resolved to true)

Fixes https://github.com/payloadcms/payload/issues/8072
2024-09-26 11:22:36 -06:00
Paul
5c2e39ef0c fix(ui): number field not being able to clear out the field's value (#8425)
Fixes #7780 

Fixes a bug where the number field won't save data if it's being removed
entirely (should be null) instead of changed to another value.

---------

Co-authored-by: PatrikKozak <patrik@payloadcms.com>
2024-09-26 08:37:23 -06:00
Riley Pearce
84d2026330 feat: preselected theme (#8354)
This PR implements the ability to attempt to force the use of light/dark
theme in the admin panel. While I am a big advocate for the benefits
that dark mode can bring to UX, it does not always suit a clients
branding needs.

Open to discussion on whether we consider this a suitable feature for
the platform. Please feel free to add to this PR as needed.

TODO:

- [x] Implement tests (I'm open to guidance on this from the Payload
team as currently it doesn't look like it's possible to adjust the
payload config file on the fly - meaning it can't be easily placed in
the admin folder tests).

---------

Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
2024-09-26 11:09:29 -03:00
60 changed files with 245 additions and 92 deletions

View File

@@ -98,6 +98,7 @@ The following options are available:
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`.
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
<Banner type="success">

View File

@@ -90,6 +90,7 @@ The Relationship Field inherits all of the default options from the base [Field
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany` is set to `true`). |
| **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. |
| **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. |
| **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More](#sortOptions) |
### Sort Options

View File

@@ -1,6 +1,6 @@
{
"name": "payload-monorepo",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"private": true,
"type": "module",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "create-payload-app",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

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

View File

@@ -1,3 +1,5 @@
import ObjectIdImport from 'bson-objectid'
import { isValidObjectId } from 'mongoose'
import {
buildVersionCollectionFields,
type CreateVersion,
@@ -10,6 +12,8 @@ import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
export const createVersion: CreateVersion = async function createVersion(
this: MongooseAdapter,
{
@@ -47,6 +51,23 @@ export const createVersion: CreateVersion = async function createVersion(
const [doc] = await VersionModel.create([data], options, req)
const parentQuery = {
$or: [
{
parent: {
$eq: data.parent,
},
},
],
}
if (typeof data.parent === 'string' && isValidObjectId(data.parent)) {
parentQuery.$or.push({
parent: {
$eq: ObjectId(data.parent),
},
})
}
await VersionModel.updateMany(
{
$and: [
@@ -55,11 +76,7 @@ export const createVersion: CreateVersion = async function createVersion(
$ne: doc._id,
},
},
{
parent: {
$eq: data.parent,
},
},
parentQuery,
{
latest: {
$eq: true,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "Vercel Postgres adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/drizzle",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "Payload Nodemailer Email Adapter",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/live-preview-react",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -12,6 +12,10 @@ type GetRequestLanguageArgs = {
const acceptedThemes: Theme[] = ['dark', 'light']
export const getRequestTheme = ({ config, cookies, headers }: GetRequestLanguageArgs): Theme => {
if (config.admin.theme !== 'all' && acceptedThemes.includes(config.admin.theme)) {
return config.admin.theme
}
const themeCookie = cookies.get(`${config.cookiePrefix || 'payload'}-theme`)
const themeFromCookie: Theme = (

View File

@@ -1,5 +1,5 @@
import type { I18n } from '@payloadcms/translations'
import type { LanguageOptions } from 'payload'
import type { Config, LanguageOptions } from 'payload'
import { FieldLabel } from '@payloadcms/ui'
import React from 'react'
@@ -14,8 +14,9 @@ export const Settings: React.FC<{
readonly className?: string
readonly i18n: I18n
readonly languageOptions: LanguageOptions
readonly theme: Config['admin']['theme']
}> = (props) => {
const { className, i18n, languageOptions } = props
const { className, i18n, languageOptions, theme } = props
return (
<div className={[baseClass, className].filter(Boolean).join(' ')}>
@@ -24,7 +25,7 @@ export const Settings: React.FC<{
<FieldLabel field={null} htmlFor="language-select" label={i18n.t('general:language')} />
<LanguageSelector languageOptions={languageOptions} />
</div>
<ToggleTheme />
{theme === 'all' && <ToggleTheme />}
</div>
)
}

View File

@@ -38,7 +38,11 @@ export const Account: React.FC<AdminViewProps> = async ({
} = initPageResult
const {
admin: { components: { views: { Account: CustomAccountComponent } = {} } = {}, user: userSlug },
admin: {
components: { views: { Account: CustomAccountComponent } = {} } = {},
theme,
user: userSlug,
},
routes: { api },
serverURL,
} = config
@@ -85,7 +89,7 @@ export const Account: React.FC<AdminViewProps> = async ({
return (
<DocumentInfoProvider
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
collectionSlug={userSlug}
docPermissions={docPermissions}

View File

@@ -113,10 +113,19 @@ export const DefaultEditView: React.FC = () => {
const isLockingEnabled = lockDocumentsProp !== false
const preventLeaveWithoutSaving =
(!(collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) ||
!(globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave)) &&
!disableLeaveWithoutSaving
let preventLeaveWithoutSaving = true
if (collectionConfig) {
preventLeaveWithoutSaving = !(
collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave
)
} else if (globalConfig) {
preventLeaveWithoutSaving = !(
globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave
)
} else if (typeof disableLeaveWithoutSaving !== 'undefined') {
preventLeaveWithoutSaving = !disableLeaveWithoutSaving
}
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
const [showTakeOverModal, setShowTakeOverModal] = useState(false)

View File

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

View File

@@ -25,6 +25,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
reset: '/reset',
unauthorized: '/unauthorized',
},
theme: 'all',
},
bin: [],
collections: [],

View File

@@ -818,6 +818,12 @@ export type Config = {
/** The route for the unauthorized page. */
unauthorized?: string
}
/**
* Restrict the Admin Panel theme to use only one of your choice
*
* @default 'all' // The theme can be configured by users
*/
theme?: 'all' | 'dark' | 'light'
/** The slug of a Collection that you want to be used to log in to the Admin dashboard. */
user?: string
}

View File

@@ -1125,6 +1125,7 @@ type SharedRelationshipPropertiesClient = FieldBaseClient &
type RelationshipAdmin = {
allowCreate?: boolean
allowEdit?: boolean
components?: {
Error?: CustomComponent<
RelationshipFieldErrorClientComponent | RelationshipFieldErrorServerComponent
@@ -1142,7 +1143,7 @@ type RelationshipAdminClient = {
Label?: MappedComponent
} & AdminClient['components']
} & AdminClient &
Pick<RelationshipAdmin, 'allowCreate' | 'isSortable'>
Pick<RelationshipAdmin, 'allowCreate' | 'allowEdit' | 'isSortable'>
export type PolymorphicRelationshipField = {
admin?: {

View File

@@ -580,26 +580,39 @@ export function fieldsToJSONSchema(
}
// This function is part of the public API and is exported through payload/utilities
export function entityToJSONSchema(
config: SanitizedConfig,
incomingEntity: SanitizedCollectionConfig | SanitizedGlobalConfig,
interfaceNameDefinitions: Map<string, JSONSchema4>,
defaultIDType: 'number' | 'text',
): JSONSchema4 {
export function entityToJSONSchema({
config,
defaultIDType,
entityType,
incomingEntity,
interfaceNameDefinitions,
}: {
config: SanitizedConfig
defaultIDType: 'number' | 'text'
entityType: 'collection' | 'global'
incomingEntity: SanitizedCollectionConfig | SanitizedGlobalConfig
interfaceNameDefinitions: Map<string, JSONSchema4>
}): JSONSchema4 {
const entity: SanitizedCollectionConfig | SanitizedGlobalConfig = deepCopyObject(incomingEntity)
const title = entity.typescript?.interface
? entity.typescript.interface
: singular(toWords(entity.slug, true))
const idField: FieldAffectingData = { name: 'id', type: defaultIDType as 'text', required: true }
const customIdField = entity.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
) as FieldAffectingData
if (entityType === 'collection') {
const idField: FieldAffectingData = {
name: 'id',
type: defaultIDType as 'text',
required: true,
}
const customIdField = entity.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
) as FieldAffectingData
if (customIdField && customIdField.type !== 'group' && customIdField.type !== 'tab') {
customIdField.required = true
} else {
entity.fields.unshift(idField)
if (customIdField && customIdField.type !== 'group' && customIdField.type !== 'tab') {
customIdField.required = true
} else {
entity.fields.unshift(idField)
}
}
// mark timestamp fields required
@@ -803,13 +816,33 @@ export function configToJSONSchema(
// Collections and Globals have to be moved to the top-level definitions as well. Reason: The top-level type will be the `Config` type - we don't want all collection and global
// types to be inlined inside the `Config` type
const entityDefinitions: { [k: string]: JSONSchema4 } = [
...config.globals,
...config.collections,
].reduce((acc, entity) => {
acc[entity.slug] = entityToJSONSchema(config, entity, interfaceNameDefinitions, defaultIDType)
return acc
}, {})
const globalsEntityDefinitions: { [k: string]: JSONSchema4 } = [...config.globals].reduce(
(acc, entity) => {
acc[entity.slug] = entityToJSONSchema({
config,
defaultIDType,
entityType: 'global',
incomingEntity: entity,
interfaceNameDefinitions,
})
return acc
},
{},
)
const collectionsEntityDefinitions: { [k: string]: JSONSchema4 } = [...config.collections].reduce(
(acc, entity) => {
acc[entity.slug] = entityToJSONSchema({
config,
defaultIDType,
entityType: 'collection',
incomingEntity: entity,
interfaceNameDefinitions,
})
return acc
},
{},
)
const authOperationDefinitions = [...config.collections]
.filter(({ auth }) => Boolean(auth))
@@ -824,7 +857,8 @@ export function configToJSONSchema(
let jsonSchema: JSONSchema4 = {
additionalProperties: false,
definitions: {
...entityDefinitions,
...globalsEntityDefinitions,
...collectionsEntityDefinitions,
...Object.fromEntries(interfaceNameDefinitions),
...authOperationDefinitions,
},

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-cloud",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "The official Payload Cloud plugin",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-form-builder",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "Form builder plugin for Payload CMS",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-nested-docs",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "Redirects plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-search",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "Search plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-seo",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "SEO plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/plugin-stripe",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "Stripe plugin for Payload",
"keywords": [
"payload",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "The officially supported Lexical richtext adapter for Payload",
"homepage": "https://payloadcms.com",
"repository": {

View File

@@ -3,6 +3,7 @@
.LexicalEditorTheme {
&__table {
border-collapse: collapse;
max-width: 100%;
border-spacing: 0;
overflow-y: scroll;
overflow-x: scroll;

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"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.0.0-beta.108",
"version": "3.0.0-beta.109",
"description": "Payload storage adapter for Amazon S3",
"homepage": "https://payloadcms.com",
"repository": {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/translations",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/ui",
"version": "3.0.0-beta.108",
"version": "3.0.0-beta.109",
"homepage": "https://payloadcms.com",
"repository": {
"type": "git",

View File

@@ -47,7 +47,7 @@ export const Autosave: React.FC<Props> = ({
} = useConfig()
const { docConfig, getVersions, versions } = useDocumentInfo()
const { reportUpdate } = useDocumentEvents()
const { dispatchFields, setModified, setSubmitted } = useForm()
const { dispatchFields, setSubmitted } = useForm()
const submitted = useFormSubmitted()
const versionsConfig = docConfig?.versions
@@ -149,7 +149,7 @@ export const Autosave: React.FC<Props> = ({
entitySlug,
updatedAt: newDate.toISOString(),
})
setModified(false)
void getVersions()
} else {
return res.json()
@@ -247,7 +247,6 @@ export const Autosave: React.FC<Props> = ({
reportUpdate,
serverURL,
setSubmitted,
setModified,
versionsConfig?.drafts,
debouncedFields,
submitted,

View File

@@ -63,7 +63,12 @@ export type ReactSelectAdapterProps = {
disabled?: boolean
filterOption?:
| ((
{ data, label, value }: { data: Option; label: string; value: string },
{
allowEdit,
data,
label,
value,
}: { allowEdit: boolean; data: Option; label: string; value: string },
search: string,
) => boolean)
| undefined

View File

@@ -78,7 +78,7 @@ const NumberFieldComponent: NumberFieldClientComponent = (props) => {
let newVal = val
if (Number.isNaN(val)) {
newVal = undefined
newVal = null
}
if (typeof onChangeFromProps === 'function') {

View File

@@ -3,11 +3,12 @@ import type { Option } from '../../elements/ReactSelect/types.js'
import type { OptionGroup, Value } from './types.js'
type Args = {
allowEdit: boolean
options: OptionGroup[]
value: Value | Value[]
}
export const findOptionsByValue = ({ options, value }: Args): Option | Option[] => {
export const findOptionsByValue = ({ allowEdit, options, value }: Args): Option | Option[] => {
if (value || typeof value === 'number') {
if (Array.isArray(value)) {
return value.map((val) => {
@@ -25,7 +26,7 @@ export const findOptionsByValue = ({ options, value }: Args): Option | Option[]
}
})
return matchedOption
return matchedOption ? { allowEdit, ...matchedOption } : undefined
})
}
@@ -42,7 +43,7 @@ export const findOptionsByValue = ({ options, value }: Args): Option | Option[]
}
})
return matchedOption
return matchedOption ? { allowEdit, ...matchedOption } : undefined
}
return undefined

View File

@@ -46,6 +46,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
_path: pathFromProps,
admin: {
allowCreate = true,
allowEdit = true,
className,
description,
isSortable = true,
@@ -576,7 +577,7 @@ const RelationshipFieldComponent: RelationshipFieldClientComponent = (props) =>
}
}, [openDrawer, currentlyOpenRelationship])
const valueToRender = findOptionsByValue({ options, value })
const valueToRender = findOptionsByValue({ allowEdit, options, value })
if (!Array.isArray(valueToRender) && valueToRender?.value === 'null') {
valueToRender.value = null

View File

@@ -24,7 +24,7 @@ export const MultiValueLabel: React.FC<
} & MultiValueProps<Option>
> = (props) => {
const {
data: { label, relationTo, value },
data: { allowEdit, label, relationTo, value },
selectProps: { customProps: { draggableProps, onDocumentDrawerOpen } = {} } = {},
} = props
@@ -44,7 +44,7 @@ export const MultiValueLabel: React.FC<
}}
/>
</div>
{relationTo && hasReadPermission && (
{relationTo && hasReadPermission && allowEdit !== false && (
<Fragment>
<button
aria-label={`Edit ${label}`}

View File

@@ -25,7 +25,7 @@ export const SingleValue: React.FC<
> = (props) => {
const {
children,
data: { label, relationTo, value },
data: { allowEdit, label, relationTo, value },
selectProps: { customProps: { onDocumentDrawerOpen } = {} } = {},
} = props
@@ -39,7 +39,7 @@ export const SingleValue: React.FC<
<div className={`${baseClass}__label`}>
<div className={`${baseClass}__label-text`}>
<div className={`${baseClass}__text`}>{children}</div>
{relationTo && hasReadPermission && (
{relationTo && hasReadPermission && allowEdit !== false && (
<Fragment>
<button
aria-label={t('general:editLabel', { label })}

View File

@@ -2,6 +2,7 @@ import type { I18nClient } from '@payloadcms/translations'
import type { ClientCollectionConfig, ClientConfig, FilterOptionsResult } from 'payload'
export type Option = {
allowEdit: boolean
label: string
options?: Option[]
relationTo?: string

View File

@@ -88,7 +88,7 @@ export const RootProvider: React.FC<Props> = ({
<ModalProvider classPrefix="payload" transTime={0} zIndex="var(--z-modal)">
<AuthProvider permissions={permissions} user={user}>
<PreferencesProvider>
<ThemeProvider cookiePrefix={config.cookiePrefix} theme={theme}>
<ThemeProvider theme={theme}>
<ParamsProvider>
<LocaleProvider>
<StepNavProvider>

View File

@@ -1,6 +1,8 @@
'use client'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { useConfig } from '../Config/index.js'
export type Theme = 'dark' | 'light'
export type ThemeContext = {
@@ -55,20 +57,26 @@ export const defaultTheme = 'light'
export const ThemeProvider: React.FC<{
children?: React.ReactNode
cookiePrefix?: string
theme?: Theme
}> = ({ children, cookiePrefix, theme: initialTheme }) => {
const cookieKey = `${cookiePrefix || 'payload'}-theme`
}> = ({ children, theme: initialTheme }) => {
const { config } = useConfig()
const preselectedTheme = config.admin.theme
const cookieKey = `${config.cookiePrefix || 'payload'}-theme`
const [theme, setThemeState] = useState<Theme>(initialTheme || defaultTheme)
const [autoMode, setAutoMode] = useState<boolean>()
useEffect(() => {
if (preselectedTheme !== 'all') {
return
}
const { theme, themeFromCookies } = getTheme(cookieKey)
setThemeState(theme)
setAutoMode(!themeFromCookies)
}, [cookieKey])
}, [preselectedTheme, cookieKey])
const setTheme = useCallback(
(themeToSet: 'auto' | Theme) => {

View File

@@ -21,6 +21,7 @@ export default buildConfigWithDefaults({
importMap: {
baseDir: path.resolve(dirname),
},
theme: 'dark',
},
cors: ['http://localhost:3000', 'http://localhost:3001'],
globals: [MenuGlobal],

View File

@@ -98,4 +98,11 @@ test.describe('Admin Panel (Root)', () => {
await expect(favicons.nth(1)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
await expect(favicons.nth(1)).toHaveAttribute('href', /\/payload-favicon-light\.[a-z\d]+\.png/)
})
test('config.admin.theme should restrict the theme', async () => {
await page.goto(url.account)
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
await expect(page.locator('#field-theme')).toBeHidden()
await expect(page.locator('#field-theme-auto')).toBeHidden()
})
})

View File

@@ -138,4 +138,16 @@ describe('Number', () => {
'The following field is invalid: withMinRows',
)
})
test('should keep data removed on save if deleted', async () => {
const input = 1
await page.goto(url.create)
const field = page.locator('#field-number')
await field.fill(String(input))
await saveDocAndAssert(page)
await expect(field).toHaveValue(String(input))
await field.fill('')
await saveDocAndAssert(page)
await expect(field).toHaveValue('')
})
})

View File

@@ -159,11 +159,32 @@ describe('relationship', () => {
test('should hide relationship add new button', async () => {
await page.goto(url.create)
await page.waitForURL(url.create)
const locator1 = page.locator(
'#relationWithAllowEditToFalse-add-new .relationship-add-new__add-button',
)
await expect(locator1).toHaveCount(1)
// expect the button to not exist in the field
const count = await page
.locator('#relationToSelfSelectOnly-add-new .relationship-add-new__add-button')
.count()
expect(count).toEqual(0)
const locator2 = page.locator(
'#relationWithAllowCreateToFalse-add-new .relationship-add-new__add-button',
)
await expect(locator2).toHaveCount(0)
})
test('should hide relationship edit button', async () => {
await page.goto(url.create)
await page.waitForURL(url.create)
const locator1 = page
.locator('#field-relationWithAllowEditToFalse')
.getByLabel('Edit dev@payloadcms.com')
await expect(locator1).toHaveCount(0)
const locator2 = page
.locator('#field-relationWithAllowCreateToFalse')
.getByLabel('Edit dev@payloadcms.com')
await expect(locator2).toHaveCount(1)
// The reason why I check for locator 1 again is that I've noticed that sometimes
// the default value does not appear after the first locator is tested. IDK why.
await expect(locator1).toHaveCount(0)
})
// TODO: Flaky test in CI - fix this. https://github.com/payloadcms/payload/actions/runs/8910825395/job/24470963991

View File

@@ -38,10 +38,25 @@ const RelationshipFields: CollectionConfig = {
},
{
name: 'relationToSelfSelectOnly',
relationTo: relationshipFieldsSlug,
type: 'relationship',
},
{
name: 'relationWithAllowCreateToFalse',
admin: {
allowCreate: false,
},
relationTo: relationshipFieldsSlug,
defaultValue: ({ user }) => user?.id,
relationTo: 'users',
type: 'relationship',
},
{
name: 'relationWithAllowEditToFalse',
admin: {
allowEdit: false,
},
defaultValue: ({ user }) => user?.id,
relationTo: 'users',
type: 'relationship',
},
{

View File

@@ -1140,6 +1140,8 @@ export interface RelationshipField {
| null;
relationToSelf?: (string | null) | RelationshipField;
relationToSelfSelectOnly?: (string | null) | RelationshipField;
relationWithAllowCreateToFalse?: (string | null) | User;
relationWithAllowEditToFalse?: (string | null) | User;
relationWithDynamicDefault?: (string | null) | User;
relationHasManyWithDynamicDefault?: {
relationTo: 'users';