Compare commits
35 Commits
v3.0.0-bet
...
fix/tab-la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e80b38ab6 | ||
|
|
4b0351fcca | ||
|
|
fa97d95675 | ||
|
|
95231daf14 | ||
|
|
8acbda078e | ||
|
|
b10f61cb25 | ||
|
|
87360f23ac | ||
|
|
8fadc3391b | ||
|
|
c6519aba8a | ||
|
|
82ba1930e5 | ||
|
|
06ea67a184 | ||
|
|
775e6e413a | ||
|
|
57f93c97a1 | ||
|
|
32c8d2821b | ||
|
|
a37abd16ac | ||
|
|
a033cfe1c4 | ||
|
|
28ea0c59e8 | ||
|
|
6da4f06205 | ||
|
|
50da2125a5 | ||
|
|
7f3d935b4d | ||
|
|
e72f12af97 | ||
|
|
a80f5b65ec | ||
|
|
dc69e2c0f6 | ||
|
|
19e2f10f4b | ||
|
|
bd41b4d7d2 | ||
|
|
fbc395b692 | ||
|
|
30eb1d522e | ||
|
|
dedcff0448 | ||
|
|
338c93a229 | ||
|
|
36ba6d47b4 | ||
|
|
c696728f64 | ||
|
|
3583c45b67 | ||
|
|
c3bc2ba4a4 | ||
|
|
040c2a2fbb | ||
|
|
b1173dc6ad |
24
.github/workflows/label-author.yml
vendored
24
.github/workflows/label-author.yml
vendored
@@ -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`);
|
||||
|
||||
1
.github/workflows/post-release.yml
vendored
1
.github/workflows/post-release.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/pr-title.yml
vendored
2
.github/workflows/pr-title.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
@@ -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
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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`}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -15,7 +15,7 @@ export const find: Find = async function find(
|
||||
{
|
||||
collection,
|
||||
joins = {},
|
||||
limit,
|
||||
limit = 0,
|
||||
locale,
|
||||
page,
|
||||
pagination,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
10
packages/db-mongodb/src/upsert.ts
Normal file
10
packages/db-mongodb/src/upsert.ts
Normal 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 })
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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' {
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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:*"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -55,6 +55,7 @@ export const buildOrderBy = ({
|
||||
pathSegments: sortPath.replace(/__/g, '.').split('.'),
|
||||
selectFields,
|
||||
tableName,
|
||||
useAlias: true,
|
||||
value: sortPath,
|
||||
})
|
||||
orderBy.column = sortTable?.[sortTableColumnName]
|
||||
|
||||
@@ -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}`]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
47
packages/drizzle/src/utilities/executeSchemaHooks.ts
Normal file
47
packages/drizzle/src/utilities/executeSchemaHooks.ts
Normal 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
|
||||
}
|
||||
}
|
||||
63
packages/drizzle/src/utilities/extendDrizzleTable.ts
Normal file
63
packages/drizzle/src/utilities/extendDrizzleTable.ts
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}>
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export type TypeWithVersion<T> = {
|
||||
createdAt: string
|
||||
id: string
|
||||
parent: number | string
|
||||
publishedLocale?: string
|
||||
snapshot?: boolean
|
||||
updatedAt: string
|
||||
version: T
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
—
|
||||
@@ -151,7 +157,6 @@ export const MetaDescriptionComponent: React.FC<MetaDescriptionProps> = (props)
|
||||
Component: null,
|
||||
RenderedComponent: errorMessage,
|
||||
}}
|
||||
label={label}
|
||||
onChange={setValue}
|
||||
path={pathFromContext}
|
||||
required={required}
|
||||
|
||||
@@ -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>
|
||||
—
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
—
|
||||
@@ -150,7 +156,6 @@ export const MetaTitleComponent: React.FC<MetaTitleProps> = (props) => {
|
||||
Component: null,
|
||||
RenderedComponent: errorMessage,
|
||||
}}
|
||||
label={label}
|
||||
onChange={setValue}
|
||||
path={pathFromContext}
|
||||
required={required}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user