Compare commits

...

35 Commits

Author SHA1 Message Date
Paul Popus
2e80b38ab6 fix(ui): issue with tab labels not changing based on language when its a function 2024-09-25 21:53:30 -06:00
Paul
4b0351fcca fix(ui): align to the top fields inside row field (#8421) 2024-09-25 22:39:30 +00:00
Jessica Chowdhury
fa97d95675 chore: add join field image to docs (#8420)
Add screenshot of join field to docs.
2024-09-25 16:03:06 -04:00
Paul
95231daf14 fix(ui): versions in documentInfo and status component reverse latest true changes (#8417) 2024-09-25 19:36:58 +00:00
Sasha
8acbda078e feat(drizzle): customize schema with before / after init hooks (#8196)
Adds abillity to customize the generated Drizzle schema with
`beforeSchemaInit` and `afterSchemaInit`. Could be useful if you want to
preserve the existing database schema / override the generated one with
features that aren't supported from the Payload config.

## Docs:

### beforeSchemaInit

Runs before the schema is built. You can use this hook to extend your
database structure with tables that won't be managed by Payload.

```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'

postgresAdapter({
  beforeSchemaInit: [
    ({ schema, adapter }) => {
      return {
        ...schema,
        tables: {
          ...schema.tables,
          addedTable: pgTable('added_table', {
            id: serial('id').notNull(),
          }),
        },
      }
    },
  ],
})
```

One use case is preserving your existing database structure when
migrating to Payload. By default, Payload drops the current database
schema, which may not be desirable in this scenario.
To quickly generate the Drizzle schema from your database you can use
[Drizzle
Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
You should get the `schema.ts` file which may look like this:

```ts
import { pgTable, uniqueIndex, serial, varchar, text } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  fullName: text('full_name'),
  phone: varchar('phone', { length: 256 }),
})

export const countries = pgTable(
  'countries',
  {
    id: serial('id').primaryKey(),
    name: varchar('name', { length: 256 }),
  },
  (countries) => {
    return {
      nameIndex: uniqueIndex('name_idx').on(countries.name),
    }
  },
)

```

You can import them into your config and append to the schema with the
`beforeSchemaInit` hook like this:

```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { users, countries } from '../drizzle/schema'

postgresAdapter({
  beforeSchemaInit: [
    ({ schema, adapter }) => {
      return {
        ...schema,
        tables: {
          ...schema.tables,
          users,
          countries
        },
      }
    },
  ],
})
```

Make sure Payload doesn't overlap table names with its collections. For
example, if you already have a collection with slug "users", you should
either change the slug or `dbName` to change the table name for this
collection.


### afterSchemaInit

Runs after the Drizzle schema is built. You can use this hook to modify
the schema with features that aren't supported by Payload, or if you
want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args.
You can refer to the [Drizzle
documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a
composite index on `country` and `city` columns.

```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { index, integer } from 'drizzle-orm/pg-core'
import { buildConfig } from 'payload'

export default buildConfig({
  collections: [
    {
      slug: 'places',
      fields: [
        {
          name: 'country',
          type: 'text',
        },
        {
          name: 'city',
          type: 'text',
        },
      ],
    },
  ],
  db: postgresAdapter({
    afterSchemaInit: [
      ({ schema, extendTable, adapter }) => {
        extendTable({
          table: schema.tables.places,
          columns: {
            extraIntegerColumn: integer('extra_integer_column'),
          },
          extraConfig: (table) => ({
            country_city_composite_index: index('country_city_composite_index').on(
              table.country,
              table.city,
            ),
          }),
        })

        return schema
      },
    ],
  }),
})

```



<!--

For external contributors, please include:

- A summary of the pull request and any related issues it fixes.
- Reasoning for the changes made or any additional context that may be
useful.

Ensure you have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.

 -->
2024-09-25 15:14:03 -04:00
Dan Ribbens
b10f61cb25 fix(db-mongodb): add req to migration templates for transactions (#8407)
- Add `req` to MigrateUpArgs and MigrateDownArgs for mongodb
- Improve docs for transactions and migrations
2024-09-25 12:58:48 -04:00
Sasha
87360f23ac fix: make field property of FieldLabel optional and partial (#8409)
Fixes https://github.com/payloadcms/payload/issues/8366
2024-09-25 11:46:33 -04:00
Paul
8fadc3391b fix(plugin-seo): titles being displayed twice (#8310) 2024-09-25 11:05:18 -04:00
Paul
c6519aba8a fix(drizzle): migrate args no longer partial payload request (#8375)
The arguments for migration up or down is now the full PayloadRequest instead of partial
2024-09-25 09:29:37 -04:00
Dan Ribbens
82ba1930e5 feat: add upsert to database interface and adapters (#8397)
- Adds the upsert method to the database interface
- Adds a mongodb specific option to extend the updateOne to accept
mongoDB Query Options (to pass `upsert: true`)
- Added upsert method to all database adapters
- Uses db.upsert in the payload preferences update operation

Includes a test using payload-preferences
2024-09-25 09:23:54 -04:00
Paul
06ea67a184 fix: client function error on forgot password view (#8374) 2024-09-24 18:00:41 -06:00
Sasha
775e6e413a fix(drizzle): use alias for localized field sorting (#8396)
Fixes https://github.com/payloadcms/payload/issues/7015
2024-09-24 16:39:00 -04:00
Patrik
57f93c97a1 fix: lock documents using the live-preview view (#8343)
Updates:
- Exports `handleGoBack`, `handleBackToDashboard`, & `handleTakeOver`
functions to consolidate logic in default edit view & live-preview edit
view.

- Only unlock document on navigation away from edit view entirely (aka
do not unlock document if switching between tabs like `edit` -->
`live-preview` --> `versions` --> `api`
2024-09-24 16:38:11 -04:00
Elliot DeNolf
32c8d2821b ci: add missing gh usernames for label-author 2024-09-24 16:03:48 -04:00
Paul
a37abd16ac fix(ui): published, draft and changed labels should now be correctly displayed (#8382)
Fixes the issue where the published or changed document is always shown
as "Changed" instead of "Published" or "Draft"


![image](https://github.com/user-attachments/assets/05581b73-0e17-4b41-96a8-007c8b6161f2)


Statuses:
- Published - when the current version is also the published version
- Changed - when the current version is a draft version but a published
version exists
- Draft - when the current version is a draft and no published versions
exist

---------

Co-authored-by: Jessica Chowdhury <jessica@trbl.design>
2024-09-24 19:48:21 +00:00
Elliot DeNolf
a033cfe1c4 ci: handle labeling CONTRIBUTOR and COLLABORATOR author associations 2024-09-24 13:47:41 -04:00
Sasha
28ea0c59e8 feat!: improve afterError hook to accept array of functions, change to object args (#8389)
Changes the `afterError` hook structure, adds tests / more docs.
Ensures that the `req.responseHeaders` property is respected in the
error handler.

**Breaking**
`afterError` now accepts an array of functions instead of a single
function:
```diff
- afterError: () => {...}
+ afterError: [() => {...}]
```

The args are changed to accept an object with the following properties:
| Argument | Description |
| ------------------- |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
| **`error`** | The error that occurred. |
| **`context`** | Custom context passed between Hooks. [More
details](./context). |
| **`graphqlResult`** | The GraphQL result object, available if the hook
is executed within a GraphQL context. |
| **`req`** | The
[Request](https://developer.mozilla.org/en-US/docs/Web/API/Request)
object containing the currently authenticated `user` |
| **`collection`** | The [Collection](../configuration/collections) in
which this Hook is running against. This will be `undefined` if the hook
is executed from a non-collection endpoint or GraphQL. |
| **`result`** | The formatted error result object, available if the
hook is executed from a REST context. |
2024-09-24 13:29:53 -04:00
Elliot DeNolf
6da4f06205 fix(db-vercel-postgres): include needed pg dependency (#8393)
`pg` appears to be a needed dependency in order for drizzle /
@vercel/postgres to build successfully.
2024-09-24 13:10:25 -04:00
Elliot DeNolf
50da2125a5 fix(templates): proper migration file import source for vercel-postgres (#8394)
- Add `ci` npm script properly for postgres templates
- Fix import source for migration files when using
`@payloadcms/db-vercel-postgres`
2024-09-24 12:24:59 -04:00
Elliot DeNolf
7f3d935b4d ci: add db-vercel-postgres as valid scope 2024-09-24 12:15:22 -04:00
Elliot DeNolf
e72f12af97 feat: templates update (#8391)
Run generate templates script
2024-09-24 11:20:35 -04:00
Alessio Gravili
a80f5b65ec fix: safely access user in auth operations (#8381) 2024-09-23 20:46:43 +00:00
Dan Ribbens
dc69e2c0f6 fix(db-mongodb): db.find default limit to 0 (#8376)
Fixes an error anytime a `db.find` is called on documents with joins
without a set `limit` by defaulting the limit to 0.
2024-09-23 16:31:50 -04:00
Germán Jabloñski
19e2f10f4b fix(richtext-lexical): regression in lexical blocks (#8378)
Fix https://github.com/payloadcms/payload/issues/8371
2024-09-23 19:58:27 +00:00
Germán Jabloñski
bd41b4d7d2 fix(richtext-lexical): table dropdown menu dark mode color (#8368)
This PR supports dark mode for the tables dropdown menu (currently only
available in light mode).

I copied the colors from
[slash-menu-popup](https://github.com/payloadcms/payload/blob/beta/packages/richtext-lexical/src/lexical/plugins/SlashMenu/index.scss)
to keep things consistent.

Below are screenshots of the change. I also show the slash-menu-popup to
compare color consistency, and the light mode to verify that it's not
broken.

## Before


![image](https://github.com/user-attachments/assets/a709bf8c-1dc2-47ac-8310-5cd1776cb268)


## After


![image](https://github.com/user-attachments/assets/e6df6693-793d-4afb-8dcc-2ead5ac62ca9)

![image](https://github.com/user-attachments/assets/7604fdcd-34d0-4801-96c2-ae5ca92357d9)

![image](https://github.com/user-attachments/assets/3bd2c877-2567-44dd-89fe-cc565988f72a)

![image](https://github.com/user-attachments/assets/813693ea-ddbe-45f5-8f98-5c9c8c58c082)
2024-09-23 14:03:14 -04:00
Sasha
fbc395b692 fix: optional sortOptions type for SingleRelationshipFieldClient (#8340)
Fixes the typescript error when using `RelationshipField` and passing
`admin` options without `sortOptions`.
2024-09-23 11:40:42 -04:00
Sasha
30eb1d522e fix(next): set the user data after first user registration (#8360)
Fixes https://github.com/payloadcms/payload/issues/8353 by analogy with
https://github.com/payloadcms/payload/pull/8135
2024-09-23 11:39:36 -04:00
Sasha
dedcff0448 fix(drizzle): sanitize query value uuid / number id NaN (#8369)
Fixes https://github.com/payloadcms/payload/issues/8347 (additionally
for UUID search as well)
2024-09-23 11:35:07 -04:00
Sasha
338c93a229 fix(drizzle): array/relationship/select hasMany in localized field (#8355)
This PR addresses these issues with localized groups / tabs with
Postgres / SQLite:

- Array fields inside of localized groups. Fixes
https://github.com/payloadcms/payload/issues/8322
- Select fields with `hasMany: true` inside of localized groups. Related
to 1, but still needed its own additional logic.
- Relationship (non-polymorphic / non has-many) inside of localized
groups. Previously, even just trying to define them in the config led to
a crash. Fixes https://github.com/payloadcms/payload/issues/8308

Ensures test coverage for localized groups.
2024-09-23 11:34:02 -04:00
Elliot DeNolf
36ba6d47b4 ci: unused debug log in post-release 2024-09-22 10:39:54 -04:00
Dan Ribbens
c696728f64 fix: cannot use join on relationships in unnamed fields (#8359)
fixes #8356
2024-09-22 08:29:58 -04:00
Tylan Davis
3583c45b67 fix(ui): inconsistent arrow dropdown on buttons, popover missing caret (#8341)
Fixes the style of the Publish and Restore buttons' dropdown triggers,
using the button's size for consistent padding of the trigger's button.
Closes #8284

| Before | After |
| :--- | :--- |
| ![Screenshot 2024-09-20 at 2 32
51 PM](https://github.com/user-attachments/assets/ae8a5788-dfd3-43d1-a066-d99722592aee)
| ![Screenshot 2024-09-20 at 2 34
27 PM](https://github.com/user-attachments/assets/16dbdfa9-9db8-4ce5-a210-bc308727b39e)
|
| ![Screenshot 2024-09-20 at 2 34
56 PM](https://github.com/user-attachments/assets/f0edc8aa-08f4-46a2-a64d-1ff2ff95abd2)
| ![Screenshot 2024-09-20 at 2 35
12 PM](https://github.com/user-attachments/assets/31e8db78-5687-43ab-82a6-c6d1db5fec5a)
|
2024-09-21 16:13:42 -04:00
Elliot DeNolf
c3bc2ba4a4 chore: bold the scope in release notes 2024-09-20 23:00:03 -04:00
Elliot DeNolf
040c2a2fbb chore(eslint): FlatConfig type deprecated, set to Config 2024-09-20 22:46:40 -04:00
Elliot DeNolf
b1173dc6ad ci: add uploadthing scope [skip ci] 2024-09-20 22:07:36 -04:00
190 changed files with 2285 additions and 584 deletions

View File

@@ -32,8 +32,26 @@ jobs:
script: |
const type = context.payload.pull_request ? 'pull_request' : 'issue';
const association = context.payload[type].author_association;
let label = ''
if (association === 'MEMBER' || association === 'OWNER') {
let label = '';
if (
association === 'MEMBER' ||
association === 'OWNER' ||
[
'denolfe',
'jmikrut',
'danribbens',
'alessiogr',
'jacobsfletch',
'jarrodmflesch',
'jesschowdhury',
'kendelljoseph',
'patrikkozak',
'paulpopus',
'r1tsuu',
'tylandavis',
].includes(context.actor.toLowerCase())
) {
label = 'created-by: Payload team';
} else if (association === 'CONTRIBUTOR') {
label = 'created-by: Contributor';
@@ -47,4 +65,4 @@ jobs:
repo: context.repo.repo,
labels: [label],
});
console.log('Added created-by: Payload team label');
console.log(`Added '${label}' label`);

View File

@@ -15,7 +15,6 @@ jobs:
fetch-depth: 0
# Only needed if debugging on a branch other than default
# ref: ${{ github.event.release.target_commitish || github.ref }}
- run: echo "npm_version=$(npm pkg get version | tr -d '"')" >> "$GITHUB_ENV"
- uses: ./.github/actions/release-commenter
continue-on-error: true
env:

View File

@@ -38,6 +38,7 @@ jobs:
db-\*
db-mongodb
db-postgres
db-vercel-postgres
db-sqlite
drizzle
email-nodemailer
@@ -62,6 +63,7 @@ jobs:
storage-\*
storage-azure
storage-gcs
storage-uploadthing
storage-vercel-blob
storage-s3
translations

View File

@@ -33,10 +33,6 @@ A migration file has two exports - an `up` function, which is called when a migr
that will be called if for some reason the migration fails to complete successfully. The `up` function should contain
all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make.
For an added level of safety, migrations should leverage Payload [transactions](/docs/database/transactions). Migration
functions should make use of the `req` by adding it to the arguments of your Payload Local API calls such
as `payload.create` and Database Adapter methods like `payload.db.create`.
Here is an example migration file:
```ts
@@ -53,6 +49,14 @@ export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
}
```
## Using Transactions
When migrations are run, each migration is performed in a new [transactions](/docs/database/transactions) for you. All
you need to do is pass the `req` object to any [local API](/docs/local-api/overview) or direct database calls, such as
`payload.db.updateMany()`, to make database changes inside the transaction. Assuming no errors were thrown, the transaction is committed
after your `up` or `down` function runs. If the migration errors at any point or fails to commit, it is caught and the
transaction gets aborted. This way no change is made to the database if the migration fails.
## Migrations Directory
Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be

View File

@@ -63,6 +63,8 @@ export default buildConfig({
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
## Access to Drizzle
@@ -97,3 +99,134 @@ Alternatively, you can disable `push` and rely solely on migrations to keep your
In Postgres, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work.
For more information about migrations, [click here](/docs/beta/database/migrations#when-to-run-migrations).
## Drizzle schema hooks
### beforeSchemaInit
Runs before the schema is built. You can use this hook to extend your database structure with tables that won't be managed by Payload.
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { integer, pgTable, serial } from 'drizzle-orm/pg-core'
postgresAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
return {
...schema,
tables: {
...schema.tables,
addedTable: pgTable('added_table', {
id: serial('id').notNull(),
}),
},
}
},
],
})
```
One use case is preserving your existing database structure when migrating to Payload. By default, Payload drops the current database schema, which may not be desirable in this scenario.
To quickly generate the Drizzle schema from your database you can use [Drizzle Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
You should get the `schema.ts` file which may look like this:
```ts
import { pgTable, uniqueIndex, serial, varchar, text } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: serial('id').primaryKey(),
fullName: text('full_name'),
phone: varchar('phone', { length: 256 }),
})
export const countries = pgTable(
'countries',
{
id: serial('id').primaryKey(),
name: varchar('name', { length: 256 }),
},
(countries) => {
return {
nameIndex: uniqueIndex('name_idx').on(countries.name),
}
},
)
```
You can import them into your config and append to the schema with the `beforeSchemaInit` hook like this:
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { users, countries } from '../drizzle/schema'
postgresAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
return {
...schema,
tables: {
...schema.tables,
users,
countries
},
}
},
],
})
```
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
```ts
import { postgresAdapter } from '@payloadcms/db-postgres'
import { index, integer } from 'drizzle-orm/pg-core'
import { buildConfig } from 'payload'
export default buildConfig({
collections: [
{
slug: 'places',
fields: [
{
name: 'country',
type: 'text',
},
{
name: 'city',
type: 'text',
},
],
},
],
db: postgresAdapter({
afterSchemaInit: [
({ schema, extendTable, adapter }) => {
extendTable({
table: schema.tables.places,
columns: {
extraIntegerColumn: integer('extra_integer_column'),
},
extraConfig: (table) => ({
country_city_composite_index: index('country_city_composite_index').on(
table.country,
table.city,
),
}),
})
return schema
},
],
}),
})
```

View File

@@ -35,7 +35,7 @@ export default buildConfig({
## Options
| Option | Description |
|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. |
| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
| `migrationDir` | Customize the directory that migrations are stored. |
@@ -44,8 +44,8 @@ export default buildConfig({
| `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '_locales'. |
| `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '_rels'. |
| `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '_v'. |
| `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) |
| `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) |
## Access to Drizzle
@@ -79,3 +79,134 @@ Alternatively, you can disable `push` and rely solely on migrations to keep your
In SQLite, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work.
For more information about migrations, [click here](/docs/beta/database/migrations#when-to-run-migrations).
## Drizzle schema hooks
### beforeSchemaInit
Runs before the schema is built. You can use this hook to extend your database structure with tables that won't be managed by Payload.
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { integer, sqliteTable } from 'drizzle-orm/sqlite-core'
sqliteAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
return {
...schema,
tables: {
...schema.tables,
addedTable: sqliteTable('added_table', {
id: integer('id').primaryKey({ autoIncrement: true }),
}),
},
}
},
],
})
```
One use case is preserving your existing database structure when migrating to Payload. By default, Payload drops the current database schema, which may not be desirable in this scenario.
To quickly generate the Drizzle schema from your database you can use [Drizzle Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
You should get the `schema.ts` file which may look like this:
```ts
import { sqliteTable, text, uniqueIndex, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
fullName: text('full_name'),
phone: text('phone', {length: 256}),
})
export const countries = sqliteTable(
'countries',
{
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name', { length: 256 }),
},
(countries) => {
return {
nameIndex: uniqueIndex('name_idx').on(countries.name),
}
},
)
```
You can import them into your config and append to the schema with the `beforeSchemaInit` hook like this:
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { users, countries } from '../drizzle/schema'
sqliteAdapter({
beforeSchemaInit: [
({ schema, adapter }) => {
return {
...schema,
tables: {
...schema.tables,
users,
countries
},
}
},
],
})
```
Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection.
### afterSchemaInit
Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config.
To extend a table, Payload exposes `extendTable` utillity to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration).
The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns.
```ts
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { index, integer } from 'drizzle-orm/sqlite-core'
import { buildConfig } from 'payload'
export default buildConfig({
collections: [
{
slug: 'places',
fields: [
{
name: 'country',
type: 'text',
},
{
name: 'city',
type: 'text',
},
],
},
],
db: sqliteAdapter({
afterSchemaInit: [
({ schema, extendTable, adapter }) => {
extendTable({
table: schema.tables.places,
columns: {
extraIntegerColumn: integer('extra_integer_column'),
},
extraConfig: (table) => ({
country_city_composite_index: index('country_city_composite_index').on(
table.country,
table.city,
),
}),
})
return schema
},
],
}),
})
```

View File

@@ -69,6 +69,48 @@ The following functions can be used for managing transactions:
`payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes.
`payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes.
Payload uses the `req` object to pass the transaction ID through to the database adapter. If you are not using the `req` object, you can make a new object to pass the transaction ID directly to database adapter methods and local API calls.
Example:
```ts
import payload from 'payload'
import config from './payload.config'
const standalonePayloadScript = async () => {
// initialize Payload
await payload.init({ config })
const transactionID = await payload.db.beginTransaction()
try {
// Make an update using the local API
await payload.update({
collection: 'posts',
data: {
some: 'data',
},
where: {
slug: { equals: 'my-slug' }
},
req: { transactionID },
})
/*
You can make additional db changes or run other functions
that need to be committed on an all or nothing basis
*/
// Commit the transaction
await payload.db.commitTransaction(transactionID)
} catch (error) {
// Rollback the transaction
await payload.db.rollbackTransaction(transactionID)
}
}
standalonePayloadScript()
```
## Disabling Transactions
If you wish to disable transactions entirely, you can do so by passing `false` as the `transactionOptions` in your database adapter configuration. All the official Payload database adapters support this option.

View File

@@ -17,6 +17,13 @@ The Join field is useful in scenarios including:
- To view and edit `Posts` belonging to a `Category`
- To work with any bi-directional relationship data
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/join.png"
srcDark="https://payloadcms.com/images/docs/fields/join-dark.png"
alt="Shows Join field in the Payload Admin Panel"
caption="Admin Panel screenshot of Join field"
/>
For the Join field to work, you must have an existing [relationship](./relationship) field in the collection you are
joining. This will reference the collection and path of the field of the related documents.
To add a Relationship Field, set the `type` to `join` in your [Field Config](./overview):

View File

@@ -46,6 +46,7 @@ export const CollectionWithHooks: CollectionConfig = {
afterRead: [(args) => {...}],
afterDelete: [(args) => {...}],
afterOperation: [(args) => {...}],
afterError: [(args) => {....}],
// Auth-enabled Hooks
beforeLogin: [(args) => {...}],
@@ -289,6 +290,30 @@ The following arguments are provided to the `afterOperation` hook:
| **`operation`** | The name of the operation that this hook is running within. |
| **`result`** | The result of the operation, before modifications. |
### afterError
The `afterError` Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc. The output can be used to transform the result object / status code.
```ts
import type { CollectionAfterErrorHook } from 'payload';
const afterDeleteHook: CollectionAfterErrorHook = async ({
req,
id,
doc,
}) => {...}
```
The following arguments are provided to the `afterError` Hook:
| Argument | Description |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`error`** | The error that occurred. |
| **`context`** | Custom context passed between Hooks. [More details](./context). |
| **`graphqlResult`** | The GraphQL result object, available if the hook is executed within a GraphQL context. |
| **`req`** | The `PayloadRequest` object that extends [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). Contains currently authenticated `user` and the Local API instance `payload`. |
| **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. |
| **`result`** | The formatted error result object, available if the hook is executed from a REST context. |
### beforeLogin
For [Auth-enabled Collections](../authentication/overview), this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation.

View File

@@ -43,7 +43,7 @@ export default buildConfig({
// ...
// highlight-start
hooks: {
afterError: () => {...}
afterError:[() => {...}]
},
// highlight-end
})
@@ -57,7 +57,7 @@ The following options are available:
### afterError
The `afterError` Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc.
The `afterError` Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc. The output can be used to transform the result object / status code.
```ts
import { buildConfig } from 'payload'
@@ -65,20 +65,23 @@ import { buildConfig } from 'payload'
export default buildConfig({
// ...
hooks: {
afterError: async ({ error }) => {
afterError: [async ({ error }) => {
// Do something
}
}]
},
})
```
The following arguments are provided to the `afterError` Hook:
| Argument | Description |
|----------|-----------------------------------------------------------------------------------------------|
| **`error`** | The error that occurred. |
| **`context`** | Custom context passed between Hooks. [More details](./context). |
| Argument | Description |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`error`** | The error that occurred. |
| **`context`** | Custom context passed between Hooks. [More details](./context). |
| **`graphqlResult`** | The GraphQL result object, available if the hook is executed within a GraphQL context. |
| **`req`** | The `PayloadRequest` object that extends [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). Contains currently authenticated `user` and the Local API instance `payload`. |
| **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. This will be `undefined` if the hook is executed from a non-collection endpoint or GraphQL. |
| **`result`** | The formatted error result object, available if the hook is executed from a REST context. |
## Async vs. Synchronous
All Hooks can be written as either synchronous or asynchronous functions. Choosing the right type depends on your use case, but switching between the two is as simple as adding or removing the `async` keyword.

View File

@@ -21,8 +21,7 @@ export const defaultESLintIgnores = [
'**/temp/',
]
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
export const rootParserOptions = {
sourceType: 'module',
@@ -33,7 +32,7 @@ export const rootParserOptions = {
},
}
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const rootEslintConfig = [
...payloadEsLintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -11,11 +11,11 @@ const migrationTemplate = ({ downSQL, imports, upSQL }: MigrationTemplateArgs):
} from '@payloadcms/db-mongodb'
${imports}
export async function up({ payload }: MigrateUpArgs): Promise<void> {
export async function up({ payload, req }: MigrateUpArgs): Promise<void> {
${upSQL ?? ` // Migration code`}
}
export async function down({ payload }: MigrateDownArgs): Promise<void> {
export async function down({ payload, req }: MigrateDownArgs): Promise<void> {
${downSQL ?? ` // Migration code`}
}
`

View File

@@ -15,7 +15,7 @@ export const find: Find = async function find(
{
collection,
joins = {},
limit,
limit = 0,
locale,
page,
pagination,

View File

@@ -1,7 +1,7 @@
import type { CollationOptions, TransactionOptions } from 'mongodb'
import type { MongoMemoryReplSet } from 'mongodb-memory-server'
import type { ClientSession, Connection, ConnectOptions } from 'mongoose'
import type { BaseDatabaseAdapter, DatabaseAdapterObj, Payload } from 'payload'
import type { ClientSession, Connection, ConnectOptions, QueryOptions } from 'mongoose'
import type { BaseDatabaseAdapter, DatabaseAdapterObj, Payload, UpdateOneArgs } from 'payload'
import fs from 'fs'
import mongoose from 'mongoose'
@@ -36,6 +36,7 @@ import { updateGlobal } from './updateGlobal.js'
import { updateGlobalVersion } from './updateGlobalVersion.js'
import { updateOne } from './updateOne.js'
import { updateVersion } from './updateVersion.js'
import { upsert } from './upsert.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
@@ -124,6 +125,7 @@ declare module 'payload' {
}[]
sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
versions: {
[slug: string]: CollectionModel
}
@@ -191,6 +193,7 @@ export function mongooseAdapter({
updateGlobalVersion,
updateOne,
updateVersion,
upsert,
})
}

View File

@@ -20,6 +20,7 @@ import type {
JSONField,
NumberField,
Payload,
PayloadRequest,
PointField,
RadioField,
RelationshipField,
@@ -109,5 +110,5 @@ export type FieldToSchemaMap<TSchema> = {
upload: FieldGeneratorFunction<TSchema, UploadField>
}
export type MigrateUpArgs = { payload: Payload }
export type MigrateDownArgs = { payload: Payload }
export type MigrateUpArgs = { payload: Payload; req: PayloadRequest }
export type MigrateDownArgs = { payload: Payload; req: PayloadRequest }

View File

@@ -1,3 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { PayloadRequest, UpdateOne } from 'payload'
import type { MongooseAdapter } from './index.js'
@@ -9,11 +10,20 @@ import { withSession } from './withSession.js'
export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter,
{ id, collection, data, locale, req = {} as PayloadRequest, where: whereArg },
{
id,
collection,
data,
locale,
options: optionsArgs = {},
req = {} as PayloadRequest,
where: whereArg,
},
) {
const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection]
const options = {
const options: QueryOptions = {
...optionsArgs,
...(await withSession(this, req)),
lean: true,
new: true,

View File

@@ -0,0 +1,10 @@
import type { PayloadRequest, Upsert } from 'payload'
import type { MongooseAdapter } from './index.js'
export const upsert: Upsert = async function upsert(
this: MongooseAdapter,
{ collection, data, locale, req = {} as PayloadRequest, where },
) {
return this.updateOne({ collection, data, locale, options: { upsert: true }, req, where })
}

View File

@@ -111,9 +111,10 @@ export const buildJoinAggregation = async ({
input: `$${as}.docs`,
},
}, // Slicing the docs to match the limit
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
}, // Boolean indicating if more docs than limit
[`${as}.hasNextPage`]: limitJoin
? { $gt: [{ $size: `$${as}.docs` }, limitJoin] }
: false,
// Boolean indicating if more docs than limit
},
},
)

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -76,6 +76,8 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
return createDatabaseAdapter<PostgresAdapter>({
name: 'postgres',
afterSchemaInit: args.afterSchemaInit ?? [],
beforeSchemaInit: args.beforeSchemaInit ?? [],
defaultDrizzleSnapshot,
drizzle: undefined,
enums: {},
@@ -150,6 +152,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
updateGlobalVersion,
updateOne,
updateVersion,
upsert: updateOne,
})
}

View File

@@ -4,6 +4,7 @@ import type {
MigrateDownArgs,
MigrateUpArgs,
PostgresDB,
PostgresSchemaHook,
} from '@payloadcms/drizzle/postgres'
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { DrizzleConfig } from 'drizzle-orm'
@@ -11,6 +12,18 @@ import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-co
import type { Pool, PoolConfig } from 'pg'
export type Args = {
/**
* Transform the schema after it's built.
* You can use it to customize the schema with features that aren't supported by Payload.
* Examples may include: composite indices, generated columns, vectors
*/
afterSchemaInit?: PostgresSchemaHook[]
/**
* Transform the schema before it's built.
* You can use it to preserve an existing database schema and if there are any collissions Payload will override them.
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
*/
beforeSchemaInit?: PostgresSchemaHook[]
idType?: 'serial' | 'uuid'
localesSuffix?: string
logger?: DrizzleConfig['logger']
@@ -41,6 +54,8 @@ declare module 'payload' {
export interface DatabaseAdapter
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
afterSchemaInit: PostgresSchemaHook[]
beforeSchemaInit: PostgresSchemaHook[]
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
drizzle: PostgresDB
enums: Record<string, GenericEnum>

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -79,6 +79,8 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
return createDatabaseAdapter<SQLiteAdapter>({
name: 'sqlite',
afterSchemaInit: args.afterSchemaInit ?? [],
beforeSchemaInit: args.beforeSchemaInit ?? [],
client: undefined,
clientConfig: args.client,
defaultDrizzleSnapshot,
@@ -151,6 +153,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
updateGlobalVersion,
updateOne,
updateVersion,
upsert: updateOne,
})
}

View File

@@ -1,7 +1,7 @@
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { Init, SanitizedCollectionConfig } from 'payload'
import { createTableName } from '@payloadcms/drizzle'
import { createTableName, executeSchemaHooks } from '@payloadcms/drizzle'
import { uniqueIndex } from 'drizzle-orm/sqlite-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import toSnakeCase from 'to-snake-case'
@@ -11,8 +11,10 @@ import type { SQLiteAdapter } from './types.js'
import { buildTable } from './schema/build.js'
export const init: Init = function init(this: SQLiteAdapter) {
export const init: Init = async function init(this: SQLiteAdapter) {
let locales: [string, ...string[]] | undefined
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
if (this.payload.config.localization) {
locales = this.payload.config.localization.locales.map(({ code }) => code) as [
string,
@@ -132,4 +134,6 @@ export const init: Init = function init(this: SQLiteAdapter) {
})
}
})
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
}

View File

@@ -879,7 +879,7 @@ export const traverseFields = ({
// add relationship to table
relationsToBuild.set(fieldName, {
type: 'one',
localized: adapter.payload.config.localization && field.localized,
localized: adapter.payload.config.localization && (field.localized || forceLocalized),
target: tableName,
})

View File

@@ -1,5 +1,5 @@
import type { Client, Config, ResultSet } from '@libsql/client'
import type { Operators } from '@payloadcms/drizzle'
import type { extendDrizzleTable, Operators } from '@payloadcms/drizzle'
import type { BuildQueryJoinAliases, DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { DrizzleConfig, Relation, Relations, SQL } from 'drizzle-orm'
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
@@ -12,7 +12,31 @@ import type {
import type { SQLiteRaw } from 'drizzle-orm/sqlite-core/query-builders/raw'
import type { Payload, PayloadRequest } from 'payload'
type SQLiteSchema = {
relations: Record<string, GenericRelation>
tables: Record<string, SQLiteTableWithColumns<any>>
}
type SQLiteSchemaHookArgs = {
extendTable: typeof extendDrizzleTable
schema: SQLiteSchema
}
export type SQLiteSchemaHook = (args: SQLiteSchemaHookArgs) => Promise<SQLiteSchema> | SQLiteSchema
export type Args = {
/**
* Transform the schema after it's built.
* You can use it to customize the schema with features that aren't supported by Payload.
* Examples may include: composite indices, generated columns, vectors
*/
afterSchemaInit?: SQLiteSchemaHook[]
/**
* Transform the schema before it's built.
* You can use it to preserve an existing database schema and if there are any collissions Payload will override them.
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
*/
beforeSchemaInit?: SQLiteSchemaHook[]
client: Config
idType?: 'serial' | 'uuid'
localesSuffix?: string
@@ -86,6 +110,8 @@ type SQLiteDrizzleAdapter = Omit<
>
export type SQLiteAdapter = {
afterSchemaInit: SQLiteSchemaHook[]
beforeSchemaInit: SQLiteSchemaHook[]
client: Client
clientConfig: Args['client']
countDistinct: CountDistinct
@@ -127,11 +153,11 @@ export type IDType = 'integer' | 'numeric' | 'text'
export type MigrateUpArgs = {
payload: Payload
req?: Partial<PayloadRequest>
req: PayloadRequest
}
export type MigrateDownArgs = {
payload: Payload
req?: Partial<PayloadRequest>
req: PayloadRequest
}
declare module 'payload' {

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -62,7 +62,8 @@
"@types/pg": "8.10.2",
"@types/to-snake-case": "1.0.0",
"esbuild": "0.23.1",
"payload": "workspace:*"
"payload": "workspace:*",
"pg": "8.11.3"
},
"peerDependencies": {
"payload": "workspace:*"

View File

@@ -76,6 +76,8 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
return createDatabaseAdapter<VercelPostgresAdapter>({
name: 'postgres',
afterSchemaInit: args.afterSchemaInit ?? [],
beforeSchemaInit: args.beforeSchemaInit ?? [],
defaultDrizzleSnapshot,
drizzle: undefined,
enums: {},
@@ -150,6 +152,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
updateGlobalVersion,
updateOne,
updateVersion,
upsert: updateOne,
})
}

View File

@@ -4,6 +4,7 @@ import type {
MigrateDownArgs,
MigrateUpArgs,
PostgresDB,
PostgresSchemaHook,
} from '@payloadcms/drizzle/postgres'
import type { DrizzleAdapter } from '@payloadcms/drizzle/types'
import type { VercelPool, VercelPostgresPoolConfig } from '@vercel/postgres'
@@ -11,6 +12,18 @@ import type { DrizzleConfig } from 'drizzle-orm'
import type { PgSchema, PgTableFn, PgTransactionConfig } from 'drizzle-orm/pg-core'
export type Args = {
/**
* Transform the schema after it's built.
* You can use it to customize the schema with features that aren't supported by Payload.
* Examples may include: composite indices, generated columns, vectors
*/
afterSchemaInit?: PostgresSchemaHook[]
/**
* Transform the schema before it's built.
* You can use it to preserve an existing database schema and if there are any collissions Payload will override them.
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
*/
beforeSchemaInit?: PostgresSchemaHook[]
connectionString?: string
idType?: 'serial' | 'uuid'
localesSuffix?: string
@@ -46,6 +59,8 @@ declare module 'payload' {
export interface DatabaseAdapter
extends Omit<Args, 'idType' | 'logger' | 'migrationDir' | 'pool'>,
DrizzleAdapter {
afterSchemaInit: PostgresSchemaHook[]
beforeSchemaInit: PostgresSchemaHook[]
beginTransaction: (options?: PgTransactionConfig) => Promise<null | number | string>
drizzle: PostgresDB
enums: Record<string, GenericEnum>

View File

@@ -32,6 +32,8 @@ export { updateGlobal } from './updateGlobal.js'
export { updateGlobalVersion } from './updateGlobalVersion.js'
export { updateVersion } from './updateVersion.js'
export { upsertRow } from './upsertRow/index.js'
export { executeSchemaHooks } from './utilities/executeSchemaHooks.js'
export { extendDrizzleTable } from './utilities/extendDrizzleTable.js'
export { hasLocalesTable } from './utilities/hasLocalesTable.js'
export { pushDevSchema } from './utilities/pushDevSchema.js'
export { validateExistingBlockIsIdentical } from './utilities/validateExistingBlockIsIdentical.js'

View File

@@ -7,9 +7,12 @@ import toSnakeCase from 'to-snake-case'
import type { BaseExtraConfig, BasePostgresAdapter } from './types.js'
import { createTableName } from '../createTableName.js'
import { executeSchemaHooks } from '../utilities/executeSchemaHooks.js'
import { buildTable } from './schema/build.js'
export const init: Init = function init(this: BasePostgresAdapter) {
export const init: Init = async function init(this: BasePostgresAdapter) {
await executeSchemaHooks({ type: 'beforeSchemaInit', adapter: this })
if (this.payload.config.localization) {
this.enums.enum__locales = this.pgSchema.enum(
'_locales',
@@ -110,4 +113,6 @@ export const init: Init = function init(this: BasePostgresAdapter) {
})
}
})
await executeSchemaHooks({ type: 'afterSchemaInit', adapter: this })
}

View File

@@ -886,7 +886,7 @@ export const traverseFields = ({
// add relationship to table
relationsToBuild.set(fieldName, {
type: 'one',
localized: adapter.payload.config.localization && field.localized,
localized: adapter.payload.config.localization && (field.localized || forceLocalized),
target: tableName,
})

View File

@@ -23,7 +23,7 @@ import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { Payload, PayloadRequest } from 'payload'
import type { QueryResult } from 'pg'
import type { Operators } from '../index.js'
import type { extendDrizzleTable, Operators } from '../index.js'
import type { BuildQueryJoinAliases, DrizzleAdapter, TransactionPg } from '../types.js'
export type BaseExtraConfig = Record<
@@ -99,7 +99,25 @@ type Schema =
}
| PgSchema
type PostgresSchema = {
enums: Record<string, GenericEnum>
relations: Record<string, GenericRelation>
tables: Record<string, PgTableWithColumns<any>>
}
type PostgresSchemaHookArgs = {
adapter: PostgresDrizzleAdapter
extendTable: typeof extendDrizzleTable
schema: PostgresSchema
}
export type PostgresSchemaHook = (
args: PostgresSchemaHookArgs,
) => PostgresSchema | Promise<PostgresSchema>
export type BasePostgresAdapter = {
afterSchemaInit: PostgresSchemaHook[]
beforeSchemaInit: PostgresSchemaHook[]
countDistinct: CountDistinct
defaultDrizzleSnapshot: DrizzleSnapshotJSON
deleteWhere: DeleteWhere
@@ -156,5 +174,5 @@ export type PostgresDrizzleAdapter = Omit<
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
export type MigrateUpArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateDownArgs = { payload: Payload; req?: Partial<PayloadRequest> }
export type MigrateUpArgs = { payload: Payload; req: PayloadRequest }
export type MigrateDownArgs = { payload: Payload; req: PayloadRequest }

View File

@@ -55,6 +55,7 @@ export const buildOrderBy = ({
pathSegments: sortPath.replace(/__/g, '.').split('.'),
selectFields,
tableName,
useAlias: true,
value: sortPath,
})
orderBy.column = sortTable?.[sortTableColumnName]

View File

@@ -53,6 +53,7 @@ type Args = {
* If creating a new table name for arrays and blocks, this suffix should be appended to the table name
*/
tableNameSuffix?: string
useAlias?: boolean
/**
* The raw value of the query before sanitization
*/
@@ -78,6 +79,7 @@ export const getTableColumnFromPath = ({
selectFields,
tableName,
tableNameSuffix = '',
useAlias,
value,
}: Args): TableColumn => {
const fieldPath = incomingSegments[0]
@@ -139,6 +141,7 @@ export const getTableColumnFromPath = ({
selectFields,
tableName: newTableName,
tableNameSuffix,
useAlias,
value,
})
}
@@ -159,6 +162,7 @@ export const getTableColumnFromPath = ({
selectFields,
tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
useAlias,
value,
})
}
@@ -177,6 +181,7 @@ export const getTableColumnFromPath = ({
selectFields,
tableName: newTableName,
tableNameSuffix,
useAlias,
value,
})
}
@@ -212,6 +217,7 @@ export const getTableColumnFromPath = ({
selectFields,
tableName: newTableName,
tableNameSuffix: `${tableNameSuffix}${toSnakeCase(field.name)}_`,
useAlias,
value,
})
}
@@ -339,6 +345,7 @@ export const getTableColumnFromPath = ({
rootTableName,
selectFields,
tableName: newTableName,
useAlias,
value,
})
}
@@ -397,6 +404,7 @@ export const getTableColumnFromPath = ({
rootTableName,
selectFields: blockSelectFields,
tableName: newTableName,
useAlias,
value,
})
} catch (error) {
@@ -633,6 +641,7 @@ export const getTableColumnFromPath = ({
rootTableName: newTableName,
selectFields,
tableName: newTableName,
useAlias,
value,
})
} else if (
@@ -685,6 +694,7 @@ export const getTableColumnFromPath = ({
pathSegments: pathSegments.slice(1),
selectFields,
tableName: newTableName,
useAlias,
value,
})
}
@@ -698,15 +708,21 @@ export const getTableColumnFromPath = ({
}
if (fieldAffectsData(field)) {
let newTable = adapter.tables[newTableName]
if (field.localized && adapter.payload.config.localization) {
// If localized, we go to localized table and set aliasTable to undefined
// so it is not picked up below to be used as targetTable
const parentTable = aliasTable || adapter.tables[tableName]
newTableName = `${tableName}${adapter.localesSuffix}`
newTable = useAlias
? getTableAlias({ adapter, tableName: newTableName }).newAliasTable
: adapter.tables[newTableName]
joins.push({
condition: eq(parentTable.id, adapter.tables[newTableName]._parentID),
table: adapter.tables[newTableName],
condition: eq(parentTable.id, newTable._parentID),
table: newTable,
})
aliasTable = undefined
@@ -714,13 +730,13 @@ export const getTableColumnFromPath = ({
if (locale !== 'all') {
constraints.push({
columnName: '_locale',
table: adapter.tables[newTableName],
table: newTable,
value: locale,
})
}
}
const targetTable = aliasTable || adapter.tables[newTableName]
const targetTable = aliasTable || newTable
selectFields[`${newTableName}.${columnPrefix}${field.name}`] =
targetTable[`${columnPrefix}${field.name}`]

View File

@@ -2,6 +2,7 @@ import type { SQL } from 'drizzle-orm'
import type { Field, Operator, Where } from 'payload'
import { and, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm'
import { PgUUID } from 'drizzle-orm/pg-core'
import { QueryError } from 'payload'
import { validOperators } from 'payload/shared'
@@ -194,6 +195,7 @@ export function parseParams({
adapter,
columns,
field,
isUUID: table?.[columnName] instanceof PgUUID,
operator,
relationOrPath,
val,

View File

@@ -16,6 +16,7 @@ type SanitizeQueryValueArgs = {
rawColumn: SQL<unknown>
}[]
field: Field | TabAsField
isUUID: boolean
operator: string
relationOrPath: string
val: any
@@ -30,6 +31,7 @@ export const sanitizeQueryValue = ({
adapter,
columns,
field,
isUUID,
operator: operatorArg,
relationOrPath,
val,
@@ -90,6 +92,16 @@ export const sanitizeQueryValue = ({
if (field.type === 'number' && typeof formattedValue === 'string') {
formattedValue = Number(val)
if (Number.isNaN(formattedValue)) {
formattedValue = null
}
}
if (isUUID && typeof formattedValue === 'string') {
if (!uuidValidate(val)) {
formattedValue = null
}
}
if (field.type === 'date' && operator !== 'exists') {

View File

@@ -527,13 +527,23 @@ export const traverseFields = <T extends Record<string, unknown>>({
return selectResult
}, {})
} else {
result[field.name] = fieldData.map(({ value }) => value)
let selectData = fieldData
if (withinArrayOrBlockLocale) {
selectData = selectData.filter(({ locale }) => locale === withinArrayOrBlockLocale)
}
result[field.name] = selectData.map(({ value }) => value)
}
}
return result
}
if (field.localized && Array.isArray(table._locales)) {
if (!table._locales.length && adapter.payload.config.localization) {
adapter.payload.config.localization.localeCodes.forEach((_locale) =>
(table._locales as unknown[]).push({ _locale }),
)
}
table._locales.forEach((localeRow) => {
valuesToTransform.push({
ref: localizedFieldData,

View File

@@ -0,0 +1,47 @@
import type { DrizzleAdapter } from '../types.js'
import { extendDrizzleTable } from './extendDrizzleTable.js'
type DatabaseSchema = {
enums?: DrizzleAdapter['enums']
relations: Record<string, any>
tables: DrizzleAdapter['tables']
}
type Adapter = {
afterSchemaInit: DatabaseSchemaHook[]
beforeSchemaInit: DatabaseSchemaHook[]
} & DatabaseSchema
type DatabaseSchemaHookArgs = {
adapter: Record<string, unknown>
extendTable: typeof extendDrizzleTable
schema: DatabaseSchema
}
type DatabaseSchemaHook = (args: DatabaseSchemaHookArgs) => DatabaseSchema | Promise<DatabaseSchema>
type Args = {
adapter: Adapter
type: 'afterSchemaInit' | 'beforeSchemaInit'
}
export const executeSchemaHooks = async ({ type, adapter }: Args): Promise<void> => {
for (const hook of adapter[type]) {
const result = await hook({
adapter,
extendTable: extendDrizzleTable,
schema: {
enums: adapter.enums,
relations: adapter.relations,
tables: adapter.tables,
},
})
if (result.enums) {
adapter.enums = result.enums
}
adapter.tables = result.tables
adapter.relations = result.relations
}
}

View File

@@ -0,0 +1,63 @@
/**
* Implemented from:
* https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/pg-core/table.ts#L73
* Drizzle uses @internal JSDoc to remove their internal methods from types, for example
* Table.Symbol, columnBuilder.build - but they actually exist.
*/
import type { ColumnBuilderBase } from 'drizzle-orm'
import { Table } from 'drizzle-orm'
import { APIError } from 'payload'
const { Symbol: DrizzleSymbol } = Table as unknown as {
Symbol: {
Columns: symbol
ExtraConfigBuilder: symbol
ExtraConfigColumns: symbol
}
}
type Args = {
columns?: Record<string, ColumnBuilderBase<any>>
extraConfig?: (self: Record<string, any>) => object
table: Table
}
/**
* Extends the passed table with additional columns / extra config
*/
export const extendDrizzleTable = ({ columns, extraConfig, table }: Args): void => {
const InlineForeignKeys = Object.getOwnPropertySymbols(table).find((symbol) => {
return symbol.description?.includes('InlineForeignKeys')
})
if (!InlineForeignKeys) {
throw new APIError(`Error when finding InlineForeignKeys Symbol`, 500)
}
if (columns) {
for (const [name, columnBuilder] of Object.entries(columns) as [string, any][]) {
const column = columnBuilder.build(table)
table[name] = column
table[InlineForeignKeys].push(...columnBuilder.buildForeignKeys(column, table))
table[DrizzleSymbol.Columns][name] = column
table[DrizzleSymbol.ExtraConfigColumns][name] =
'buildExtraConfigColumn' in columnBuilder
? columnBuilder.buildExtraConfigColumn(table)
: column
}
}
if (extraConfig) {
const originalExtraConfigBuilder = table[DrizzleSymbol.ExtraConfigBuilder]
table[DrizzleSymbol.ExtraConfigBuilder] = (t) => {
return {
...originalExtraConfigBuilder(t),
...extraConfig(t),
}
}
}
}

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -107,8 +107,7 @@ const typescriptRules = {
'@typescript-eslint/no-empty-object-type': 'warn',
}
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig} */
const baseExtends = deepMerge(
@@ -117,7 +116,7 @@ const baseExtends = deepMerge(
regexpPluginConfigs['flat/recommended'],
)
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const rootEslintConfig = [
{
name: 'Settings',

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -2,10 +2,9 @@ import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
import reactCompiler from 'eslint-plugin-react-compiler'
const { rules } = reactCompiler
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,5 +1,5 @@
import type { GraphQLError, GraphQLFormattedError } from 'graphql'
import type { CollectionAfterErrorHook, Payload, SanitizedConfig } from 'payload'
import type { APIError, Payload, PayloadRequest, SanitizedConfig } from 'payload'
import { configToSchema } from '@payloadcms/graphql'
import { createHandler } from 'graphql-http/lib/use/fetch'
@@ -11,28 +11,30 @@ import { createPayloadRequest } from '../../utilities/createPayloadRequest.js'
import { headersWithCors } from '../../utilities/headersWithCors.js'
import { mergeHeaders } from '../../utilities/mergeHeaders.js'
const handleError = async (
payload: Payload,
err: any,
debug: boolean,
afterErrorHook: CollectionAfterErrorHook,
// eslint-disable-next-line @typescript-eslint/require-await
): Promise<GraphQLFormattedError> => {
const status = err.originalError.status || httpStatus.INTERNAL_SERVER_ERROR
const handleError = async ({
err,
payload,
req,
}: {
err: GraphQLError
payload: Payload
req: PayloadRequest
}): Promise<GraphQLFormattedError> => {
const status = (err.originalError as APIError).status || httpStatus.INTERNAL_SERVER_ERROR
let errorMessage = err.message
payload.logger.error(err.stack)
// Internal server errors can contain anything, including potentially sensitive data.
// Therefore, error details will be hidden from the response unless `config.debug` is `true`
if (!debug && status === httpStatus.INTERNAL_SERVER_ERROR) {
if (!payload.config.debug && status === httpStatus.INTERNAL_SERVER_ERROR) {
errorMessage = 'Something went wrong.'
}
let response: GraphQLFormattedError = {
extensions: {
name: err?.originalError?.name || undefined,
data: (err && err.originalError && err.originalError.data) || undefined,
stack: debug ? err.stack : undefined,
data: (err && err.originalError && (err.originalError as APIError).data) || undefined,
stack: payload.config.debug ? err.stack : undefined,
statusCode: status,
},
locations: err.locations,
@@ -40,9 +42,20 @@ const handleError = async (
path: err.path,
}
if (afterErrorHook) {
;({ response } = afterErrorHook(err, response, null, null) || { response })
}
await payload.config.hooks.afterError?.reduce(async (promise, hook) => {
await promise
const result = await hook({
context: req.context,
error: err,
graphqlResult: response,
req,
})
if (result) {
response = result.graphqlResult || response
}
}, Promise.resolve())
return response
}
@@ -95,9 +108,6 @@ export const POST =
const { payload } = req
const afterErrorHook =
typeof payload.config.hooks.afterError === 'function' ? payload.config.hooks.afterError : null
const headers = {}
const apiResponse = await createHandler({
context: { headers, req },
@@ -113,7 +123,7 @@ export const POST =
if (response.errors) {
const errors = (await Promise.all(
result.errors.map((error) => {
return handleError(payload, error, payload.config.debug, afterErrorHook)
return handleError({ err: error, payload, req })
}),
)) as GraphQLError[]
// errors type should be FormattedGraphQLError[] but onOperation has a return type of ExecutionResult instead of FormattedExecutionResult

View File

@@ -1,14 +1,13 @@
import type { Collection, PayloadRequest, SanitizedConfig } from 'payload'
import type { Collection, ErrorResult, PayloadRequest, SanitizedConfig } from 'payload'
import httpStatus from 'http-status'
import { APIError, APIErrorName, ValidationErrorName } from 'payload'
import { getPayloadHMR } from '../../utilities/getPayloadHMR.js'
import { headersWithCors } from '../../utilities/headersWithCors.js'
import { mergeHeaders } from '../../utilities/mergeHeaders.js'
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResponse => {
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResult => {
if (incoming) {
// Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965
// Instead, get the prototype of the incoming error and check its constructor name
@@ -73,14 +72,14 @@ export const routeError = async ({
collection,
config: configArg,
err,
req,
req: incomingReq,
}: {
collection?: Collection
config: Promise<SanitizedConfig> | SanitizedConfig
err: APIError
req: Partial<PayloadRequest>
req: PayloadRequest | Request
}) => {
let payload = req?.payload
let payload = 'payload' in incomingReq && incomingReq?.payload
if (!payload) {
try {
@@ -95,6 +94,8 @@ export const routeError = async ({
}
}
const req = incomingReq as PayloadRequest
req.payload = payload
const headers = headersWithCors({
headers: new Headers(),
@@ -119,26 +120,44 @@ export const routeError = async ({
response.stack = err.stack
}
if (collection && typeof collection.config.hooks.afterError === 'function') {
;({ response, status } = collection.config.hooks.afterError(
err,
response,
req?.context,
collection.config,
) || { response, status })
if (collection) {
await collection.config.hooks.afterError?.reduce(async (promise, hook) => {
await promise
const result = await hook({
collection: collection.config,
context: req.context,
error: err,
req,
result: response,
})
if (result) {
response = (result.response as ErrorResult) || response
status = result.status || status
}
}, Promise.resolve())
}
if (typeof config.hooks.afterError === 'function') {
;({ response, status } = config.hooks.afterError(
err,
response,
req?.context,
collection?.config,
) || {
response,
status,
await config.hooks.afterError?.reduce(async (promise, hook) => {
await promise
const result = await hook({
collection: collection?.config,
context: req.context,
error: err,
req,
result: response,
})
}
return Response.json(response, { headers, status })
if (result) {
response = (result.response as ErrorResult) || response
status = result.status || status
}
}, Promise.resolve())
return Response.json(response, {
headers: req.responseHeaders ? mergeHeaders(req.responseHeaders, headers) : headers,
status,
})
}

View File

@@ -1,5 +1,10 @@
'use client'
import type { ClientCollectionConfig, FormState, LoginWithUsernameOptions } from 'payload'
import type {
ClientCollectionConfig,
ClientUser,
FormState,
LoginWithUsernameOptions,
} from 'payload'
import {
ConfirmPasswordField,
@@ -8,6 +13,7 @@ import {
FormSubmit,
PasswordField,
RenderFields,
useAuth,
useConfig,
useTranslation,
} from '@payloadcms/ui'
@@ -30,6 +36,7 @@ export const CreateFirstUserClient: React.FC<{
} = useConfig()
const { t } = useTranslation()
const { setUser } = useAuth()
const collectionConfig = getEntityConfig({ collectionSlug: userSlug }) as ClientCollectionConfig
@@ -50,12 +57,17 @@ export const CreateFirstUserClient: React.FC<{
[apiRoute, userSlug, serverURL],
)
const handleFirstRegister = (data: { user: ClientUser }) => {
setUser(data.user)
}
return (
<Form
action={`${serverURL}${apiRoute}/${userSlug}/first-register`}
initialState={initialState}
method="POST"
onChange={[onChange]}
onSuccess={handleFirstRegister}
redirect={admin}
validationOperation="create"
>

View File

@@ -17,7 +17,13 @@ import {
useEditDepth,
useUploadEdits,
} from '@payloadcms/ui'
import { formatAdminURL, getFormState } from '@payloadcms/ui/shared'
import {
formatAdminURL,
getFormState,
handleBackToDashboard,
handleGoBack,
handleTakeOver,
} from '@payloadcms/ui/shared'
import { useRouter, useSearchParams } from 'next/navigation.js'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
@@ -151,89 +157,6 @@ export const DefaultEditView: React.FC = () => {
return false
})
const handleTakeOver = useCallback(() => {
if (!isLockingEnabled) {
return
}
try {
// Call updateDocumentEditor to update the document's owner to the current user
void updateDocumentEditor(id, collectionSlug ?? globalSlug, user)
documentLockStateRef.current.hasShownLockedModal = true
// Update the locked state to reflect the current user as the owner
documentLockStateRef.current = {
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal,
isLocked: true,
user,
}
setCurrentEditor(user)
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error during document takeover:', error)
}
}, [
updateDocumentEditor,
id,
collectionSlug,
globalSlug,
user,
setCurrentEditor,
isLockingEnabled,
])
const handleTakeOverWithinDoc = useCallback(() => {
if (!isLockingEnabled) {
return
}
try {
// Call updateDocumentEditor to update the document's owner to the current user
void updateDocumentEditor(id, collectionSlug ?? globalSlug, user)
// Update the locked state to reflect the current user as the owner
documentLockStateRef.current = {
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal,
isLocked: true,
user,
}
setCurrentEditor(user)
// Ensure the document is editable for the incoming user
setIsReadOnlyForIncomingUser(false)
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error during document takeover:', error)
}
}, [
updateDocumentEditor,
id,
collectionSlug,
globalSlug,
user,
setCurrentEditor,
isLockingEnabled,
])
const handleGoBack = useCallback(() => {
const redirectRoute = formatAdminURL({
adminRoute,
path: collectionSlug ? `/collections/${collectionSlug}` : '/',
})
router.push(redirectRoute)
}, [adminRoute, collectionSlug, router])
const handleBackToDashboard = useCallback(() => {
setShowTakeOverModal(false)
const redirectRoute = formatAdminURL({
adminRoute,
path: '/',
})
router.push(redirectRoute)
}, [adminRoute, router])
const onSave = useCallback(
(json) => {
reportUpdate({
@@ -373,7 +296,19 @@ export const DefaultEditView: React.FC = () => {
return
}
if ((id || globalSlug) && documentIsLocked) {
const currentPath = window.location.pathname
const documentId = id || globalSlug
// Routes where we do NOT want to unlock the document
const stayWithinDocumentPaths = ['preview', 'api', 'versions']
const isStayingWithinDocument = stayWithinDocumentPaths.some((path) =>
currentPath.includes(path),
)
// Unlock the document only if we're actually navigating away from the document
if (documentId && documentIsLocked && !isStayingWithinDocument) {
// Check if this user is still the current editor
if (documentLockStateRef.current?.user?.id === user.id) {
void unlockDocument(id, collectionSlug ?? globalSlug)
@@ -421,20 +356,32 @@ export const DefaultEditView: React.FC = () => {
{BeforeDocument}
{isLockingEnabled && shouldShowDocumentLockedModal && !isReadOnlyForIncomingUser && (
<DocumentLocked
handleGoBack={handleGoBack}
handleGoBack={() => handleGoBack({ adminRoute, collectionSlug, router })}
isActive={shouldShowDocumentLockedModal}
onReadOnly={() => {
setIsReadOnlyForIncomingUser(true)
setShowTakeOverModal(false)
}}
onTakeOver={handleTakeOver}
onTakeOver={() =>
handleTakeOver(
id,
collectionSlug,
globalSlug,
user,
false,
updateDocumentEditor,
setCurrentEditor,
documentLockStateRef,
isLockingEnabled,
)
}
updatedAt={lastUpdateTime}
user={currentEditor}
/>
)}
{isLockingEnabled && showTakeOverModal && (
<DocumentTakeOver
handleBackToDashboard={handleBackToDashboard}
handleBackToDashboard={() => handleBackToDashboard({ adminRoute, router })}
isActive={showTakeOverModal}
onReadOnly={() => {
setIsReadOnlyForIncomingUser(true)
@@ -469,7 +416,20 @@ export const DefaultEditView: React.FC = () => {
onDrawerCreate={onDrawerCreate}
onDuplicate={onDuplicate}
onSave={onSave}
onTakeOver={handleTakeOverWithinDoc}
onTakeOver={() =>
handleTakeOver(
id,
collectionSlug,
globalSlug,
user,
true,
updateDocumentEditor,
setCurrentEditor,
documentLockStateRef,
isLockingEnabled,
setIsReadOnlyForIncomingUser,
)
}
permissions={docPermissions}
readOnlyForIncomingUser={isReadOnlyForIncomingUser}
redirectAfterDelete={redirectAfterDelete}
@@ -494,6 +454,7 @@ export const DefaultEditView: React.FC = () => {
requirePassword={!id}
setSchemaPath={setSchemaPath}
setValidateBeforeSubmit={setValidateBeforeSubmit}
// eslint-disable-next-line react-compiler/react-compiler
useAPIKey={auth.useAPIKey}
username={data?.username}
verify={auth.verify}

View File

@@ -1,7 +1,7 @@
import type { AdminViewProps } from 'payload'
import { Button, Translation } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { Button } from '@payloadcms/ui'
import { formatAdminURL, Translation } from '@payloadcms/ui/shared'
import LinkImport from 'next/link.js'
import React, { Fragment } from 'react'

View File

@@ -5,6 +5,7 @@ import type {
ClientConfig,
ClientField,
ClientGlobalConfig,
ClientUser,
Data,
LivePreviewConfig,
} from 'payload'
@@ -21,9 +22,17 @@ import {
useDocumentInfo,
useTranslation,
} from '@payloadcms/ui'
import { getFormState } from '@payloadcms/ui/shared'
import React, { Fragment, useCallback } from 'react'
import {
getFormState,
handleBackToDashboard,
handleGoBack,
handleTakeOver,
} from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { DocumentLocked } from '../../elements/DocumentLocked/index.js'
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
@@ -63,9 +72,11 @@ const PreviewView: React.FC<Props> = ({
BeforeDocument,
BeforeFields,
collectionSlug,
currentEditor,
disableActions,
disableLeaveWithoutSaving,
docPermissions,
documentIsLocked,
getDocPreferences,
globalSlug,
hasPublishPermission,
@@ -75,6 +86,10 @@ const PreviewView: React.FC<Props> = ({
isEditing,
isInitializing,
onSave: onSaveFromProps,
setCurrentEditor,
setDocumentIsLocked,
unlockDocument,
updateDocumentEditor,
} = useDocumentInfo()
const operation = id ? 'update' : 'create'
@@ -82,13 +97,36 @@ const PreviewView: React.FC<Props> = ({
const {
config: {
admin: { user: userSlug },
routes: { admin: adminRoute },
},
} = useConfig()
const router = useRouter()
const { t } = useTranslation()
const { previewWindowType } = useLivePreviewContext()
const { refreshCookieAsync, user } = useAuth()
const { reportUpdate } = useDocumentEvents()
const docConfig = collectionConfig || globalConfig
const lockDocumentsProp = docConfig?.lockDocuments !== undefined ? docConfig?.lockDocuments : true
const isLockingEnabled = lockDocumentsProp !== false
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
const documentLockStateRef = useRef<{
hasShownLockedModal: boolean
isLocked: boolean
user: ClientUser
} | null>({
hasShownLockedModal: false,
isLocked: false,
user: null,
})
const [lastUpdateTime, setLastUpdateTime] = useState(Date.now())
const onSave = useCallback(
(json) => {
reportUpdate({
@@ -103,6 +141,11 @@ const PreviewView: React.FC<Props> = ({
void refreshCookieAsync()
}
// Unlock the document after save
if ((id || globalSlug) && isLockingEnabled) {
setDocumentIsLocked(false)
}
if (typeof onSaveFromProps === 'function') {
void onSaveFromProps({
...json,
@@ -110,47 +153,194 @@ const PreviewView: React.FC<Props> = ({
})
}
},
[collectionSlug, id, onSaveFromProps, refreshCookieAsync, reportUpdate, user, userSlug],
[
collectionSlug,
globalSlug,
id,
isLockingEnabled,
onSaveFromProps,
refreshCookieAsync,
reportUpdate,
setDocumentIsLocked,
user,
userSlug,
],
)
const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
const currentTime = Date.now()
const timeSinceLastUpdate = currentTime - lastUpdateTime
const updateLastEdited = isLockingEnabled && timeSinceLastUpdate >= 10000 // 10 seconds
if (updateLastEdited) {
setLastUpdateTime(currentTime)
}
const docPreferences = await getDocPreferences()
const { state } = await getFormState({
const { lockedState, state } = await getFormState({
apiRoute,
body: {
id,
collectionSlug,
docPreferences,
formState: prevFormState,
globalSlug,
operation,
returnLockStatus: isLockingEnabled ? true : false,
schemaPath,
updateLastEdited,
},
serverURL,
})
setDocumentIsLocked(true)
if (isLockingEnabled) {
const previousOwnerId = documentLockStateRef.current?.user?.id
if (lockedState) {
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
setShowTakeOverModal(true)
documentLockStateRef.current.hasShownLockedModal = true
}
documentLockStateRef.current = documentLockStateRef.current = {
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
isLocked: true,
user: lockedState.user,
}
setCurrentEditor(lockedState.user)
}
}
}
return state
},
[serverURL, apiRoute, id, operation, schemaPath, getDocPreferences],
[
collectionSlug,
globalSlug,
serverURL,
apiRoute,
id,
isLockingEnabled,
lastUpdateTime,
operation,
schemaPath,
getDocPreferences,
setCurrentEditor,
setDocumentIsLocked,
user,
],
)
// Clean up when the component unmounts or when the document is unlocked
useEffect(() => {
return () => {
if (!isLockingEnabled) {
return
}
const currentPath = window.location.pathname
const documentId = id || globalSlug
// Routes where we do NOT want to unlock the document
const stayWithinDocumentPaths = ['preview', 'api', 'versions']
const isStayingWithinDocument = stayWithinDocumentPaths.some((path) =>
currentPath.includes(path),
)
// Unlock the document only if we're actually navigating away from the document
if (documentId && documentIsLocked && !isStayingWithinDocument) {
// Check if this user is still the current editor
if (documentLockStateRef.current?.user?.id === user.id) {
void unlockDocument(id, collectionSlug ?? globalSlug)
setDocumentIsLocked(false)
setCurrentEditor(null)
}
}
setShowTakeOverModal(false)
}
}, [
collectionSlug,
globalSlug,
id,
unlockDocument,
user.id,
setCurrentEditor,
isLockingEnabled,
documentIsLocked,
setDocumentIsLocked,
])
const shouldShowDocumentLockedModal =
documentIsLocked &&
currentEditor &&
currentEditor.id !== user.id &&
!isReadOnlyForIncomingUser &&
!showTakeOverModal &&
// eslint-disable-next-line react-compiler/react-compiler
!documentLockStateRef.current?.hasShownLockedModal
return (
<OperationProvider operation={operation}>
<Form
action={action}
className={`${baseClass}__form`}
disabled={!hasSavePermission}
disabled={isReadOnlyForIncomingUser || !hasSavePermission}
initialState={initialState}
isInitializing={isInitializing}
method={id ? 'PATCH' : 'POST'}
onChange={[onChange]}
onSuccess={onSave}
>
{isLockingEnabled && shouldShowDocumentLockedModal && !isReadOnlyForIncomingUser && (
<DocumentLocked
handleGoBack={() => handleGoBack({ adminRoute, collectionSlug, router })}
isActive={shouldShowDocumentLockedModal}
onReadOnly={() => {
setIsReadOnlyForIncomingUser(true)
setShowTakeOverModal(false)
}}
onTakeOver={() =>
handleTakeOver(
id,
collectionSlug,
globalSlug,
user,
false,
updateDocumentEditor,
setCurrentEditor,
documentLockStateRef,
isLockingEnabled,
)
}
updatedAt={lastUpdateTime}
user={currentEditor}
/>
)}
{isLockingEnabled && showTakeOverModal && (
<DocumentTakeOver
handleBackToDashboard={() => handleBackToDashboard({ adminRoute, router })}
isActive={showTakeOverModal}
onReadOnly={() => {
setIsReadOnlyForIncomingUser(true)
setShowTakeOverModal(false)
}}
/>
)}
{((collectionConfig &&
!(collectionConfig.versions?.drafts && collectionConfig.versions?.drafts?.autosave)) ||
(globalConfig &&
!(globalConfig.versions?.drafts && globalConfig.versions?.drafts?.autosave))) &&
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
!disableLeaveWithoutSaving &&
!isReadOnlyForIncomingUser && <LeaveWithoutSaving />}
<SetDocumentStepNav
collectionSlug={collectionSlug}
globalLabel={globalConfig?.label}
@@ -174,8 +364,24 @@ const PreviewView: React.FC<Props> = ({
hasSavePermission={hasSavePermission}
id={id}
isEditing={isEditing}
onTakeOver={() =>
handleTakeOver(
id,
collectionSlug,
globalSlug,
user,
true,
updateDocumentEditor,
setCurrentEditor,
documentLockStateRef,
isLockingEnabled,
setIsReadOnlyForIncomingUser,
)
}
permissions={docPermissions}
readOnlyForIncomingUser={isReadOnlyForIncomingUser}
slug={collectionConfig?.slug || globalConfig?.slug}
user={currentEditor}
/>
<div
className={[baseClass, previewWindowType === 'popup' && `${baseClass}--detached`]
@@ -197,7 +403,7 @@ const PreviewView: React.FC<Props> = ({
docPermissions={docPermissions}
fields={fields}
forceSidebarWrap
readOnly={!hasSavePermission}
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
schemaPath={collectionSlug || globalSlug}
/>
{AfterDocument}

View File

@@ -95,31 +95,23 @@ const Restore: React.FC<Props> = ({
return (
<Fragment>
<div className={[baseClass, className].filter(Boolean).join(' ')}>
<Pill
<Button
buttonStyle="pill"
className={[canRestoreAsDraft && `${baseClass}__button`].filter(Boolean).join(' ')}
onClick={() => toggleModal(modalSlug)}
>
{t('version:restoreThisVersion')}
</Pill>
{canRestoreAsDraft && (
<Popup
button={
<Pill className={`${baseClass}__chevron`}>
<ChevronIcon />
</Pill>
}
caret={false}
render={() => (
size="small"
SubMenuPopupContent={
canRestoreAsDraft && (
<PopupList.ButtonGroup>
<PopupList.Button onClick={() => [setDraft(true), toggleModal(modalSlug)]}>
{t('version:restoreAsDraft')}
</PopupList.Button>
</PopupList.ButtonGroup>
)}
size="large"
verticalAlign="bottom"
/>
)}
)
}
>
{t('version:restoreThisVersion')}
</Button>
</div>
<Modal className={`${baseClass}__modal`} slug={modalSlug}>
<div className={`${baseClass}__wrapper`}>

View File

@@ -1,9 +1,9 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -42,6 +42,12 @@ export type DocumentTabConfig = {
readonly Pill?: PayloadComponent
}
export type ClientDocumentTabConfig = {
condition?: never
isActive?: boolean
label?: string
} & DocumentTabConfig
export type DocumentTabComponent = PayloadComponent<{
path: string
}>

View File

@@ -13,9 +13,9 @@ export type GenericLabelProps = {
}
export type FieldLabelClientProps<
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,
TFieldClient extends Partial<ClientFieldWithOptionalType> = Partial<ClientFieldWithOptionalType>,
> = {
field: TFieldClient
field?: TFieldClient
} & GenericLabelProps
export type FieldLabelServerProps<

View File

@@ -11,6 +11,7 @@ export type { CustomPublishButton } from './elements/PublishButton.js'
export type { CustomSaveButton } from './elements/SaveButton.js'
export type { CustomSaveDraftButton } from './elements/SaveDraftButton.js'
export type {
ClientDocumentTabConfig,
DocumentTabComponent,
DocumentTabCondition,
DocumentTabConfig,

View File

@@ -8,7 +8,7 @@ import type { Locale, MetaConfig, PayloadComponent } from '../../config/types.js
import type { SanitizedGlobalConfig } from '../../globals/config/types.js'
import type { PayloadRequest } from '../../types/index.js'
import type { LanguageOptions } from '../LanguageOptions.js'
import type { MappedComponent } from '../types.js'
import type { ClientDocumentTabConfig, MappedComponent } from '../types.js'
export type AdminViewConfig = {
Component: AdminViewComponent
@@ -23,6 +23,7 @@ export type AdminViewConfig = {
export type MappedView = {
actions?: MappedComponent[]
Component: MappedComponent
tab?: ClientDocumentTabConfig
}
export type AdminViewProps = {

View File

@@ -173,13 +173,14 @@ export const loginOperation = async <TSlug extends CollectionSlug>(
req,
where: whereConstraint,
})
user.collection = collectionConfig.slug
if (!user || (args.collection.config.auth.verify && user._verified === false)) {
throw new AuthenticationError(req.t, Boolean(canLoginWithUsername && sanitizedUsername))
}
if (user && isLocked(new Date(user.lockUntil).getTime())) {
user.collection = collectionConfig.slug
if (isLocked(new Date(user.lockUntil).getTime())) {
throw new LockedAuth(req.t)
}

View File

@@ -37,7 +37,10 @@ export const meOperation = async (args: Arguments): Promise<MeOperationResult> =
req,
showHiddenFields: false,
})) as User
user.collection = collection.config.slug
if (user) {
user.collection = collection.config.slug
}
if (req.user.collection !== collection.config.slug) {
return {

View File

@@ -75,7 +75,10 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
depth: isGraphQL ? 0 : args.collection.config.auth.depth,
req: args.req,
})
user.collection = args.req.user.collection
if (user) {
user.collection = args.req.user.collection
}
let result: Result

View File

@@ -16,6 +16,8 @@ import type {
import type { Auth, ClientUser, IncomingAuthType } from '../../auth/types.js'
import type {
Access,
AfterErrorHookArgs,
AfterErrorResult,
CustomComponent,
EditConfig,
Endpoint,
@@ -178,14 +180,6 @@ export type AfterOperationHook<TOperationGeneric extends CollectionSlug = string
>
>
export type AfterErrorHook = (
err: Error,
res: unknown,
context: RequestContext,
/** The collection which this hook is being run on. This is null if the AfterError hook was be added to the payload-wide config */
collection: null | SanitizedCollectionConfig,
) => { response: any; status: number } | void
export type BeforeLoginHook<T extends TypeWithID = any> = (args: {
/** The collection which this hook is being run on */
collection: SanitizedCollectionConfig
@@ -237,6 +231,10 @@ export type AfterRefreshHook<T extends TypeWithID = any> = (args: {
token: string
}) => any
export type AfterErrorHook = (
args: { collection: SanitizedCollectionConfig } & AfterErrorHookArgs,
) => AfterErrorResult | Promise<AfterErrorResult>
export type AfterForgotPasswordHook = (args: {
args?: any
/** The collection which this hook is being run on */
@@ -402,7 +400,7 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
hooks?: {
afterChange?: AfterChangeHook[]
afterDelete?: AfterDeleteHook[]
afterError?: AfterErrorHook
afterError?: AfterErrorHook[]
afterForgotPassword?: AfterForgotPasswordHook[]
afterLogin?: AfterLoginHook[]
afterLogout?: AfterLogoutHook[]

View File

@@ -6,6 +6,7 @@ import type {
} from '@payloadcms/translations'
import type { BusboyConfig } from 'busboy'
import type GraphQL from 'graphql'
import type { GraphQLFormattedError } from 'graphql'
import type { JSONSchema4 } from 'json-schema'
import type { DestinationStream, pino } from 'pino'
import type React from 'react'
@@ -23,7 +24,6 @@ import type {
InternalImportMap,
} from '../bin/generateImportMap/index.js'
import type {
AfterErrorHook,
Collection,
CollectionConfig,
SanitizedCollectionConfig,
@@ -31,7 +31,7 @@ import type {
import type { DatabaseAdapterResult } from '../database/types.js'
import type { EmailAdapter, SendEmailOptions } from '../email/types.js'
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
import type { Payload, TypedUser } from '../index.js'
import type { Payload, RequestContext, TypedUser } from '../index.js'
import type { PayloadRequest, Where } from '../types/index.js'
import type { PayloadLogger } from '../utilities/logger.js'
@@ -621,6 +621,33 @@ export type FetchAPIFileUploadOptions = {
useTempFiles?: boolean | undefined
} & Partial<BusboyConfig>
export type ErrorResult = { data?: any; errors: unknown[]; stack?: string }
export type AfterErrorResult = {
graphqlResult?: GraphQLFormattedError
response?: Partial<ErrorResult> & Record<string, unknown>
status?: number
} | void
export type AfterErrorHookArgs = {
/** The Collection that the hook is operating on. This will be undefined if the hook is executed from a non-collection endpoint or GraphQL. */
collection?: SanitizedCollectionConfig
/** Custom context passed between hooks */
context: RequestContext
/** The error that occurred. */
error: Error
/** The GraphQL result object, available if the hook is executed within a GraphQL context. */
graphqlResult?: GraphQLFormattedError
/** The Request object containing the currently authenticated user. */
req: PayloadRequest
/** The formatted error result object, available if the hook is executed from a REST context. */
result?: ErrorResult
}
export type AfterErrorHook = (
args: AfterErrorHookArgs,
) => AfterErrorResult | Promise<AfterErrorResult>
/**
* This is the central configuration
*
@@ -895,7 +922,7 @@ export type Config = {
* @see https://payloadcms.com/docs/hooks/overview
*/
hooks?: {
afterError?: AfterErrorHook
afterError?: AfterErrorHook[]
}
/** i18n config settings */
// eslint-disable-next-line @typescript-eslint/no-empty-object-type

View File

@@ -135,6 +135,8 @@ export interface BaseDatabaseAdapter {
updateOne: UpdateOne
updateVersion: UpdateVersion
upsert: Upsert
}
export type Init = () => Promise<void> | void
@@ -380,6 +382,10 @@ export type UpdateOneArgs = {
draft?: boolean
joins?: JoinQuery
locale?: string
/**
* Additional database adapter specific options to pass to the query
*/
options?: Record<string, unknown>
req: PayloadRequest
} & (
| {
@@ -394,6 +400,17 @@ export type UpdateOneArgs = {
export type UpdateOne = (args: UpdateOneArgs) => Promise<Document>
export type UpsertArgs = {
collection: string
data: Record<string, unknown>
joins?: JoinQuery
locale?: string
req: PayloadRequest
where: Where
}
export type Upsert = (args: UpsertArgs) => Promise<Document>
export type DeleteOneArgs = {
collection: string
joins?: JoinQuery

View File

@@ -41,6 +41,9 @@ export const sanitizeJoinField = ({
// Traverse fields and match based on the schema path
traverseFields({
callback: ({ field, next }) => {
if (!('name' in field) || !field.name) {
return
}
const currentSegment = pathSegments[currentSegmentIndex]
// match field on path segments
if ('name' in field && field.name === currentSegment) {

View File

@@ -1166,7 +1166,7 @@ export type SingleRelationshipField = {
} & SharedRelationshipProperties
export type SingleRelationshipFieldClient = {
admin?: Pick<SingleRelationshipField['admin'], 'sortOptions'> & RelationshipAdminClient
admin?: Partial<Pick<SingleRelationshipField['admin'], 'sortOptions'>> & RelationshipAdminClient
} & Pick<SingleRelationshipField, 'relationTo'> &
SharedRelationshipPropertiesClient

View File

@@ -24,6 +24,7 @@ import type {
DataFromCollectionSlug,
TypeWithID,
} from './collections/config/types.js'
export type * from './admin/types.js'
import type { Options as CountOptions } from './collections/operations/local/count.js'
import type { Options as CreateOptions } from './collections/operations/local/create.js'
import type {
@@ -31,6 +32,7 @@ import type {
ManyOptions as DeleteManyOptions,
Options as DeleteOptions,
} from './collections/operations/local/delete.js'
export type { MappedView } from './admin/views/types.js'
import type { Options as DuplicateOptions } from './collections/operations/local/duplicate.js'
import type { Options as FindOptions } from './collections/operations/local/find.js'
import type { Options as FindByIDOptions } from './collections/operations/local/findByID.js'
@@ -654,8 +656,6 @@ interface RequestContext {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface DatabaseAdapter extends BaseDatabaseAdapter {}
export type { Payload, RequestContext }
export type * from './admin/types.js'
export type { MappedView } from './admin/views/types.js'
export { default as executeAccess } from './auth/executeAccess.js'
export { executeAuthStrategies } from './auth/executeAuthStrategies.js'
export { getAccessResults } from './auth/getAccessResults.js'
@@ -687,7 +687,6 @@ export type {
VerifyConfig,
} from './auth/types.js'
export { generateImportMap } from './bin/generateImportMap/index.js'
export type { ImportMap } from './bin/generateImportMap/index.js'
export { genImportMapIterateFields } from './bin/generateImportMap/iterateFields.js'
export type { ClientCollectionConfig } from './collections/config/client.js'
@@ -822,6 +821,7 @@ export type {
UpdateOneArgs,
UpdateVersion,
UpdateVersionArgs,
Upsert,
} from './database/types.js'
export type { EmailAdapter as PayloadEmailAdapter, SendEmailOptions } from './email/types.js'
export {

View File

@@ -35,23 +35,10 @@ export async function update(args: PreferenceUpdateRequest) {
value,
}
let result
try {
// try/catch because we attempt to update without first reading to check if it exists first to save on db calls
result = await payload.db.updateOne({
collection,
data: preference,
req,
where,
})
} catch (err: unknown) {
result = await payload.db.create({
collection,
data: preference,
req,
})
}
return result
return await payload.db.upsert({
collection,
data: preference,
req,
where,
})
}

View File

@@ -36,6 +36,7 @@ export type TypeWithVersion<T> = {
createdAt: string
id: string
parent: number | string
publishedLocale?: string
snapshot?: boolean
updatedAt: string
version: T

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -16,10 +16,12 @@ export const sentryPlugin =
config.hooks = {
...(incomingConfig.hooks || {}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
afterError: (err: any) => {
captureException(err)
},
afterError: [
({ error }) => {
captureException(error)
},
],
}
config.onInit = async (payload) => {

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -100,7 +100,13 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
}}
>
<div className="plugin-seo__field">
<FieldLabel field={null} Label={Label} label={label} {...(labelProps || {})} />
<FieldLabel
field={null}
Label={Label}
label={label}
required={required}
{...(labelProps || {})}
/>
{hasGenerateDescriptionFn && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
@@ -151,7 +157,6 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
Component: null,
RenderedComponent: errorMessage,
}}
label={label}
onChange={setValue}
path={pathFromContext}
required={required}

View File

@@ -102,7 +102,13 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
}}
>
<div className="plugin-seo__field">
<FieldLabel field={null} Label={Label} label={label} {...(labelProps || {})} />
<FieldLabel
field={null}
Label={Label}
label={label}
required={required}
{...(labelProps || {})}
/>
{hasGenerateImageFn && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
@@ -151,7 +157,6 @@ export const MetaImageComponent: React.FC<MetaImageProps> = (props) => {
RenderedComponent: errorMessage,
}}
filterOptions={field.filterOptions}
label={undefined}
onChange={(incomingImage) => {
if (incomingImage !== null) {
if (typeof incomingImage === 'object') {

View File

@@ -37,11 +37,11 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
label,
required,
},
field: fieldFromProps,
hasGenerateTitleFn,
labelProps,
} = props || {}
const { path: pathFromContext } = useFieldProps()
const { t } = useTranslation<PluginSEOTranslations, PluginSEOTranslationKeys>()
const field: FieldType<string> = useField({
@@ -98,7 +98,13 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
}}
>
<div className="plugin-seo__field">
<FieldLabel field={null} Label={Label} label={label} {...(labelProps || {})} />
<FieldLabel
field={fieldFromProps}
Label={Label}
label={label}
required={required}
{...(labelProps || {})}
/>
{hasGenerateTitleFn && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
@@ -150,7 +156,6 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
Component: null,
RenderedComponent: errorMessage,
}}
label={label}
onChange={setValue}
path={pathFromContext}
required={required}

View File

@@ -78,7 +78,11 @@ export const PreviewComponent: React.FC<PreviewProps> = (props) => {
}, [fields, href, locale, docInfo, hasGenerateURLFn, getData])
return (
<div>
<div
style={{
marginBottom: '20px',
}}
>
<div>{t('plugin-seo:preview')}</div>
<div
style={{

View File

@@ -28,7 +28,7 @@ export const seoPlugin =
OverviewField({}),
MetaTitleField({
hasGenerateFn: typeof pluginConfig?.generateTitle === 'function',
overrides: pluginConfig?.fieldOverrides?.title as unknown as TextField,
overrides: pluginConfig?.fieldOverrides?.title,
}),
MetaDescriptionField({
hasGenerateFn: typeof pluginConfig?.generateDescription === 'function',

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -3,10 +3,9 @@ import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
import reactCompiler from 'eslint-plugin-react-compiler'
const { rules } = reactCompiler
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -57,9 +57,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
// Field Schema
useEffect(() => {
const awaitInitialState = async () => {
if (!id) {
return
}
const { state } = await getFormState({
apiRoute: config.routes.api,
body: {
@@ -90,9 +87,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
const onChange = useCallback(
async ({ formState: prevFormState }) => {
if (!id) {
throw new Error('No ID found')
}
const { state: formState } = await getFormState({
apiRoute: config.routes.api,
body: {

View File

@@ -24,7 +24,9 @@
html[data-theme='light'] {
.table-action-menu-dropdown {
@include shadow-m;
box-shadow:
0px 1px 2px 1px rgba(0, 0, 0, 0.05),
0px 4px 8px 0px rgba(0, 0, 0, 0.1);
}
}
@@ -32,17 +34,26 @@ html[data-theme='light'] {
z-index: 100;
display: block;
position: fixed;
background: var(--color-base-0);
background: var(--theme-input-bg);
min-width: 160px;
color: var(--color-base-800);
border-radius: $style-radius-m;
min-height: 40px;
overflow-y: auto;
box-shadow:
0px 1px 2px 1px rgba(0, 0, 0, 0.1),
0px 4px 16px 0px rgba(0, 0, 0, 0.2),
0px -4px 8px 0px rgba(0, 0, 0, 0.1);
hr {
border: none;
height: 1px;
background-color: var(--theme-elevation-200);
}
.item {
padding: 8px;
color: var(--color-base-900);
background: var(--color-base-0);
color: var(--theme-elevation-900);
background: var(--theme-input-bg);
cursor: pointer;
font-size: 13px;
font-family: var(--font-body);
@@ -56,7 +67,7 @@ html[data-theme='light'] {
width: 100%;
&:hover {
background: var(--color-base-100);
background: var(--theme-elevation-100);
}
}
}

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,9 +1,8 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

View File

@@ -1,10 +1,9 @@
import { rootEslintConfig, rootParserOptions } from '../../eslint.config.js'
import reactCompiler from 'eslint-plugin-react-compiler'
const { rules } = reactCompiler
/** @typedef {import('eslint').Linter.FlatConfig} */
let FlatConfig
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {FlatConfig[]} */
/** @type {Config[]} */
export const index = [
...rootEslintConfig,
{

Some files were not shown because too many files have changed in this diff Show More