Compare commits

..

13 Commits

Author SHA1 Message Date
Elliot DeNolf
c39472259a chore(release): db-postgres/0.1.13 [skip ci] 2023-11-08 14:53:16 -05:00
Elliot DeNolf
e2d36c3cab chore(release): db-mongodb/1.0.7 [skip ci] 2023-11-08 14:53:05 -05:00
Elliot DeNolf
0e682a32c3 chore(release): payload/2.1.0 [skip ci] 2023-11-08 14:51:29 -05:00
Hulpoi George-Valentin
266c3274d0 feat: Custom Error, Label, and before/after field components (#3747)
Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
2023-11-08 14:40:31 -05:00
Jarrod Flesch
67b3baaa44 fix: vite not replacing env vars correctly when building 2023-11-08 14:23:58 -05:00
Jarrod Flesch
55659c7c36 chore(docs): imporoves usability of useAuth and exports useTableColumns 2023-11-08 14:23:22 -05:00
Jørgen Kalsnes Hagen
6a0a859563 feat: add internationalization (i18n) to locales (#4005) 2023-11-08 12:56:15 -05:00
Dan Ribbens
57da3c99a7 fix: error on graphql multiple queries (#3985) 2023-11-08 12:38:25 -05:00
Elliot DeNolf
611438177b ci: split e2e tests into 8 parts 2023-11-08 12:35:05 -05:00
Jacob Fletcher
d068ef7e24 fix: injects array and block ids into fieldSchemaToJSON (#4043) 2023-11-08 12:34:51 -05:00
Jacob Fletcher
7a9af4417a fix: polymorphic hasMany relationships missing in postgres admin (#4053) 2023-11-08 12:31:07 -05:00
Patrik
8d14c213c8 fix: resets list filter row when the filter on field is changed (#3956)
Co-authored-by: Jarrod Flesch <jarrodmflesch@gmail.com>
2023-11-08 08:31:01 -05:00
Jarrod Flesch
182c57b191 fix: hasMany number and select fields unable to save within arrays (#4047) 2023-11-07 22:29:41 -05:00
101 changed files with 1480 additions and 420 deletions

View File

@@ -1,21 +0,0 @@
name: Restore build cache
description: Installes node, pnpm, and restores the build cache
runs:
using: composite
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}

View File

@@ -92,8 +92,22 @@ jobs:
POSTGRES_DB: payloadtests
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/restore-build-cache@v1
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Start PostgreSQL
uses: CasperWA/postgresql-action@v1.2
@@ -128,11 +142,25 @@ jobs:
strategy:
fail-fast: false
matrix:
part: [1/4, 2/4, 3/4, 4/4]
part: [1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8]
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/restore-build-cache@v1
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: E2E Tests
uses: nick-fields/retry@v2
@@ -154,8 +182,22 @@ jobs:
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/restore-build-cache@v1
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Generate Payload Types
run: pnpm dev:generate-types fields
@@ -180,8 +222,22 @@ jobs:
- live-preview-react
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/restore-build-cache@v1
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build ${{ matrix.pkg }}
run: pnpm turbo run build --filter=${{ matrix.pkg }}
@@ -202,8 +258,22 @@ jobs:
- plugin-sentry
steps:
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/restore-build-cache@v1
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
- name: Build ${{ matrix.pkg }}
run: pnpm turbo run build --filter=${{ matrix.pkg }}

View File

@@ -1,3 +1,25 @@
## [2.1.0](https://github.com/payloadcms/payload/compare/v2.0.15...v2.1.0) (2023-11-08)
### Features
* add internationalization (i18n) to locales ([#4005](https://github.com/payloadcms/payload/issues/4005)) ([6a0a859](https://github.com/payloadcms/payload/commit/6a0a859563ed9e742260ea51a1839a1ef0f61fce))
* Custom Error, Label, and before/after field components ([#3747](https://github.com/payloadcms/payload/issues/3747)) ([266c327](https://github.com/payloadcms/payload/commit/266c3274d03e4fd52c692eeef1ee9248dcf66189))
### Bug Fixes
* error on graphql multiple queries ([#3985](https://github.com/payloadcms/payload/issues/3985)) ([57da3c9](https://github.com/payloadcms/payload/commit/57da3c99a7e4ce5d3d1e17315e3691815f363704))
* focal and cropping issues, adds test ([#4039](https://github.com/payloadcms/payload/issues/4039)) ([acba5e4](https://github.com/payloadcms/payload/commit/acba5e482b7ddc6e3dc6ba9b7736022770d69a55))
* handle invalid tokens in refresh token operation ([#3647](https://github.com/payloadcms/payload/issues/3647)) ([131d89c](https://github.com/payloadcms/payload/commit/131d89c3f50c237e1ab2d7cd32d7a8226a9f8ce3))
* hasMany number and select fields unable to save within arrays ([#4047](https://github.com/payloadcms/payload/issues/4047)) ([182c57b](https://github.com/payloadcms/payload/commit/182c57b191010ce3dcf659f39c1dc2f7cf80662e))
* injects array and block ids into fieldSchemaToJSON ([#4043](https://github.com/payloadcms/payload/issues/4043)) ([d068ef7](https://github.com/payloadcms/payload/commit/d068ef7e2483d49dc41bdd7735042ddcaa0a684c))
* parse predefined migrations via file arg or name prefix ([#4001](https://github.com/payloadcms/payload/issues/4001)) ([eb42c03](https://github.com/payloadcms/payload/commit/eb42c031ef980558ed051d4163925aa28d6ab090))
* polymorphic hasMany relationships missing in postgres admin ([#4053](https://github.com/payloadcms/payload/issues/4053)) ([7a9af44](https://github.com/payloadcms/payload/commit/7a9af4417a56c621f01195f9a2904b9adffaad7a))
* resets list filter row when the filter on field is changed ([#3956](https://github.com/payloadcms/payload/issues/3956)) ([8d14c21](https://github.com/payloadcms/payload/commit/8d14c213c878a1afda2b3bf03431fed5aa2a44e3))
* Update API Views ([b008b6c](https://github.com/payloadcms/payload/commit/b008b6c6463c9dc3d8e61eaa0a9210aa1a189442))
* vite not replacing env vars correctly when building ([67b3baa](https://github.com/payloadcms/payload/commit/67b3baaa445a13246be8178d57eaeba92888bef1))
## [2.0.15](https://github.com/payloadcms/payload/compare/v2.0.14...v2.0.15) (2023-11-03)

View File

@@ -432,6 +432,15 @@ All Payload fields support the ability to swap in your own React components. So,
| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) |
| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) |
As an alternative to replacing the entire Field component, you may want to keep the majority of the default Field component and only swap components within. This allows you to replace the **`Label`** or **`Error`** within a field component or add additional components inside the field with **`BeforeInput`** or **`AfterInput`**. **`BeforeInput`** and **`AfterInput`** are allowed in any fields that don't contain other fields, except [UI](/docs/fields/ui) and [Rich Text](/docs/fields/rich-text).
| Component | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
| **`Label`** | Override the default Label in the Field Component. [More](#label-component) |
| **`Error`** | Override the default Label in the Field Component. [More](#error-component) |
| **`BeforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
| **`AfterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
## Cell Component
These are the props that will be passed to your custom Cell to use in your own components.
@@ -487,6 +496,103 @@ const CustomTextField: React.FC<Props> = ({ path }) => {
components, including the <strong>useField</strong> hook, [click here](/docs/admin/hooks).
</Banner>
## Label Component
These are the props that will be passed to your custom Label.
| Property | Description |
| ---------------- | ---------------------------------------------------------------- |
| **`htmlFor`** | Property used to set `for` attribute for label. |
| **`label`** | Label value provided in field, it can be used with i18n. |
| **`required`** | A boolean value that represents if the field is required or not. |
#### Example
```tsx
import React from 'react'
import { useTranslation } from 'react-i18next'
import { getTranslation } from 'payload/utilities/getTranslation'
type Props = {
htmlFor?: string
label?: Record<string, string> | false | string
required?: boolean
}
const CustomLabel: React.FC<Props> = (props) => {
const { htmlFor, label, required = false } = props
const { i18n } = useTranslation()
if (label) {
return (<span>
{getTranslation(label, i18n)}
{required && <span className="required">*</span>}
</span>);
}
return null
}
```
## Error Component
These are the props that will be passed to your custom Error.
| Property | Description |
| ---------------- | ------------------------------------------------------------- |
| **`message`** | The error message. |
| **`showError`** | A boolean value that represents if the error should be shown. |
#### Example
```tsx
import React from 'react'
type Props = {
message: string
showError?: boolean
}
const CustomError: React.FC<Props> = (props) => {
const { message, showError } = props
if (showError) {
return <p style={{color: 'red'}}>{message}</p>
} else return null;
}
```
## AfterInput and BeforeInput
With these properties you can add multiple components before and after the input element. For example, you can add an absolutely positioned button to clear the current field value.
#### Example
```tsx
import React from 'react'
import './style.scss'
const ClearButton: React.FC = () => {
return <button onClick={() => {/* ... */}}>X</button>
}
const fieldField: Field = {
name: 'title',
type: 'text',
admin: {
components: {
AfterInput: [
<ClearButton />
]
}
}
}
export default titleField;
```
## Custom providers
As your admin customizations gets more complex you may want to share state between fields or other components. You can add custom providers to do add your own context to any Payload app for use in other custom components within the admin panel. Within your config add `admin.components.providers`, these can be used to share context or provide other custom functionality. Read the [React context](https://reactjs.org/docs/context.html) docs to learn more.

View File

@@ -758,3 +758,29 @@ const MyComponent: React.FC = () => {
### usePreferences
Returns methods to set and get user preferences. More info can be found [here](https://payloadcms.com/docs/admin/preferences).
### useTableColumns
Returns methods to manipulate table columns
```tsx
import { useTableColumns } from 'payload/components/utilities'
const MyComponent: React.FC = () => {
// highlight-start
const { setActiveColumns } = useTableColumns()
const resetColumns = () => {
setActiveColumns(['id', 'createdAt', 'updatedAt'])
}
// highlight-end
return (
<button
type="button"
onClick={resetColumns}
>
Reset columns
</button>
)
}

View File

@@ -57,6 +57,38 @@ export default buildConfig({
})
```
**Example Payload config set up for localization with full locales objects (including [internationalization](/docs/configuration/i18n) support):**
```ts
import { buildConfig } from 'payload/config'
export default buildConfig({
collections: [
// collections go here
],
localization: {
locales: [
{
label: {
en: 'English', // English label
nb: 'Engelsk', // Norwegian label
},
code: 'en',
},
{
label: {
en: 'Norwegian', // English label
nb: 'Norsk', // Norwegian label
},
code: 'nb',
},
],
defaultLocale: 'en',
fallback: true,
},
})
```
**Here is a brief explanation of each of the options available within the `localization` property:**
**`locales`**

View File

@@ -63,7 +63,6 @@ export const getViteConfig = async (payloadConfig: SanitizedConfig): Promise<Inl
'module.hot': 'undefined',
'process.argv': '[]',
'process.cwd': 'function () { return "/" }',
'process.env': '{}',
'process?.cwd': 'function () { return "/" }',
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "1.0.6",
"version": "1.0.7",
"description": "The officially supported MongoDB database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -4,6 +4,7 @@ export const commitTransaction: CommitTransaction = async function commitTransac
if (!this.sessions[id]?.inTransaction()) {
return
}
await this.sessions[id].commitTransaction()
await this.sessions[id].endSession()
delete this.sessions[id]

View File

@@ -3,10 +3,20 @@ import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '',
) {
if (!this.sessions[id]?.inTransaction()) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) {
return
}
// when session exists but is not inTransaction something unexpected is happening to the session
if (!this.sessions[id].inTransaction()) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
delete this.sessions[id]
return
}
// the first call for rollback should be aborted and deleted causing any other operations with the same transaction to fail
await this.sessions[id].abortTransaction()
await this.sessions[id].endSession()
delete this.sessions[id]

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "0.1.12",
"version": "0.1.13",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",

View File

@@ -23,6 +23,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!collection?.versions?.drafts,
disableUnique: false,
@@ -37,6 +38,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!collection.versions?.drafts,
disableUnique: true,
@@ -52,6 +54,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!global?.versions?.drafts,
disableUnique: false,
@@ -66,6 +69,7 @@ export const init: Init = async function init(this: PostgresAdapter) {
buildTable({
adapter: this,
buildNumbers: true,
buildRelationships: true,
disableNotNull: !!global.versions?.drafts,
disableUnique: true,

View File

@@ -26,6 +26,7 @@ type Args = {
adapter: PostgresAdapter
baseColumns?: Record<string, PgColumnBuilder>
baseExtraConfig?: Record<string, (cols: GenericColumns) => IndexBuilder | UniqueConstraintBuilder>
buildNumbers?: boolean
buildRelationships?: boolean
disableNotNull: boolean
disableUnique: boolean
@@ -39,6 +40,7 @@ type Args = {
}
type Result = {
hasManyNumberField: 'index' | boolean
relationsToBuild: Map<string, string>
}
@@ -46,6 +48,7 @@ export const buildTable = ({
adapter,
baseColumns = {},
baseExtraConfig = {},
buildNumbers,
buildRelationships,
disableNotNull,
disableUnique = false,
@@ -53,10 +56,11 @@ export const buildTable = ({
rootRelationsToBuild,
rootRelationships,
rootTableIDColType,
rootTableName,
rootTableName: incomingRootTableName,
tableName,
timestamps,
}: Args): Result => {
const rootTableName = incomingRootTableName || tableName
const columns: Record<string, PgColumnBuilder> = baseColumns
const indexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
@@ -102,6 +106,7 @@ export const buildTable = ({
hasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columns,
disableNotNull,
@@ -116,7 +121,7 @@ export const buildTable = ({
relationships,
rootRelationsToBuild: rootRelationsToBuild || relationsToBuild,
rootTableIDColType: rootTableIDColType || idColType,
rootTableName: rootTableName || tableName,
rootTableName,
}))
if (timestamps) {
@@ -185,8 +190,8 @@ export const buildTable = ({
adapter.relations[`relations_${localeTableName}`] = localesTableRelations
}
if (hasManyNumberField) {
const numbersTableName = `${tableName}_numbers`
if (hasManyNumberField && buildNumbers) {
const numbersTableName = `${rootTableName}_numbers`
const columns: Record<string, PgColumnBuilder> = {
id: serial('id').primaryKey(),
number: numeric('number'),
@@ -327,5 +332,5 @@ export const buildTable = ({
adapter.relations[`relations_${tableName}`] = tableRelations
return { relationsToBuild }
return { hasManyNumberField, relationsToBuild }
}

View File

@@ -1,23 +1,24 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
import { relations } from 'drizzle-orm'
import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { relations } from 'drizzle-orm'
import {
PgNumericBuilder,
PgVarcharBuilder,
boolean,
index,
integer,
jsonb,
numeric,
pgEnum,
PgNumericBuilder,
PgVarcharBuilder,
text,
timestamp,
varchar,
} from 'drizzle-orm/pg-core'
import type { Field, TabAsField } from 'payload/types'
import { fieldAffectsData, optionIsObject } from 'payload/types'
import { InvalidConfiguration } from 'payload/errors'
import { fieldAffectsData, optionIsObject } from 'payload/types'
import toSnakeCase from 'to-snake-case'
import type { GenericColumns, PostgresAdapter } from '../types'
@@ -31,6 +32,7 @@ import { validateExistingBlockIsIdentical } from './validateExistingBlockIsIdent
type Args = {
adapter: PostgresAdapter
buildNumbers: boolean
buildRelationships: boolean
columnPrefix?: string
columns: Record<string, PgColumnBuilder>
@@ -60,6 +62,7 @@ type Result = {
export const traverseFields = ({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,
@@ -283,19 +286,25 @@ export const traverseFields = ({
baseExtraConfig._localeIdx = (cols) => index('_locale_idx').on(cols._locale)
}
const { relationsToBuild: subRelationsToBuild } = buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
})
const { hasManyNumberField: subHasManyNumberField, relationsToBuild: subRelationsToBuild } =
buildTable({
adapter,
baseColumns,
baseExtraConfig,
disableNotNull: disableNotNullFromHere,
disableUnique,
fields: disableUnique ? idToUUID(field.fields) : field.fields,
rootRelationsToBuild,
rootRelationships: relationships,
rootTableIDColType,
rootTableName,
tableName: arrayTableName,
})
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField
}
relationsToBuild.set(fieldName, arrayTableName)
@@ -351,7 +360,10 @@ export const traverseFields = ({
baseExtraConfig._localeIdx = (cols) => index('locale_idx').on(cols._locale)
}
const { relationsToBuild: subRelationsToBuild } = buildTable({
const {
hasManyNumberField: subHasManyNumberField,
relationsToBuild: subRelationsToBuild,
} = buildTable({
adapter,
baseColumns,
baseExtraConfig,
@@ -365,6 +377,11 @@ export const traverseFields = ({
tableName: blockTableName,
})
if (subHasManyNumberField) {
if (!hasManyNumberField || subHasManyNumberField === 'index')
hasManyNumberField = subHasManyNumberField
}
const blockTableRelations = relations(
adapter.tables[blockTableName],
({ many, one }) => {
@@ -413,6 +430,7 @@ export const traverseFields = ({
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,
@@ -449,6 +467,7 @@ export const traverseFields = ({
hasManyNumberField: groupHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix: `${columnName}_`,
columns,
@@ -486,6 +505,7 @@ export const traverseFields = ({
hasManyNumberField: tabHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,
@@ -524,6 +544,7 @@ export const traverseFields = ({
hasManyNumberField: rowHasManyNumberField,
} = traverseFields({
adapter,
buildNumbers,
buildRelationships,
columnPrefix,
columns,

View File

@@ -1,6 +1,7 @@
import type { CommitTransaction } from 'payload/database'
export const commitTransaction: CommitTransaction = async function commitTransaction(id) {
// if the session was deleted it has already been aborted
if (!this.sessions[id]) {
return
}

View File

@@ -3,12 +3,15 @@ import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '',
) {
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
return
}
// end the session promise in failure by calling reject
await this.sessions[id].reject()
// delete the session causing any other operations with the same transaction to fail
delete this.sessions[id]
}

View File

@@ -3,16 +3,18 @@ import { isArrayOfRows } from '../../utilities/isArrayOfRows'
type Args = {
data: unknown
id?: unknown
locale?: string
}
export const transformSelects = ({ data, locale }: Args) => {
export const transformSelects = ({ id, data, locale }: Args) => {
const newRows: Record<string, unknown>[] = []
if (isArrayOfRows(data)) {
data.forEach((value, i) => {
const newRow: Record<string, unknown> = {
order: i + 1,
parent: id,
value,
}

View File

@@ -422,6 +422,7 @@ export const traverseFields = ({
Object.entries(data[field.name]).forEach(([localeKey, localeData]) => {
if (Array.isArray(localeData)) {
const newRows = transformSelects({
id: data._uuid || data.id,
data: localeData,
locale: localeKey,
})
@@ -432,6 +433,7 @@ export const traverseFields = ({
}
} else if (Array.isArray(data[field.name])) {
const newRows = transformSelects({
id: data._uuid || data.id,
data: data[field.name],
})

View File

@@ -102,7 +102,9 @@ export const upsertRow = async <T extends TypeWithID>({
if (Object.keys(rowToInsert.selects).length > 0) {
Object.entries(rowToInsert.selects).forEach(([selectTableName, selectRows]) => {
selectRows.forEach((row) => {
row.parent = insertedRow.id
if (typeof row.parent === 'undefined') {
row.parent = insertedRow.id
}
if (!selectsToInsert[selectTableName]) selectsToInsert[selectTableName] = []
selectsToInsert[selectTableName].push(row)
})

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "2.0.15",
"version": "2.1.0",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"main": "./dist/index.js",

View File

@@ -1,28 +1,33 @@
import React from 'react'
import { Chevron } from '../../..'
import { useLocale } from '../../../utilities/Locale'
import { useTranslation } from 'react-i18next'
import { Chevron } from '../../..'
import { getTranslation } from '../../../../../utilities/getTranslation'
import { useLocale } from '../../../utilities/Locale'
import './index.scss'
const baseClass = 'localizer-button'
export const LocalizerLabel: React.FC<{
className?: string
ariaLabel?: string
className?: string
}> = (props) => {
const { className, ariaLabel } = props
const { ariaLabel, className } = props
const locale = useLocale()
const { t } = useTranslation('general')
const { i18n } = useTranslation()
return (
<div
className={[baseClass, className].filter(Boolean).join(' ')}
aria-label={ariaLabel || t('locale')}
className={[baseClass, className].filter(Boolean).join(' ')}
>
<div className={`${baseClass}__label`}>{`${t('locale')}:`}</div>
&nbsp;&nbsp;
<span className={`${baseClass}__current-label`}>{`${locale.label}`}</span>
<span className={`${baseClass}__current-label`}>{`${getTranslation(
locale.label,
i18n,
)}`}</span>
&nbsp;
<Chevron className={`${baseClass}__chevron`} />
</div>

View File

@@ -1,6 +1,8 @@
import qs from 'qs'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { getTranslation } from '../../../../utilities/getTranslation'
import { useConfig } from '../../utilities/Config'
import { useLocale } from '../../utilities/Locale'
import { useSearchParams } from '../../utilities/SearchParams'
@@ -18,9 +20,12 @@ const Localizer: React.FC<{
const config = useConfig()
const { localization } = config
const { i18n } = useTranslation()
const locale = useLocale()
const searchParams = useSearchParams()
const localeLabel = getTranslation(locale.label, i18n)
if (localization) {
const { locales } = localization
@@ -44,8 +49,8 @@ const Localizer: React.FC<{
}),
}}
>
{locale.label}
{locale.label !== locale.code && ` (${locale.code})`}
{localeLabel}
{localeLabel !== locale.code && ` (${locale.code})`}
</PopupList.Button>
) : null}
@@ -57,11 +62,12 @@ const Localizer: React.FC<{
locale: localeOption.code,
}
const search = qs.stringify(newParams)
const localeOptionLabel = getTranslation(localeOption.label, i18n)
return (
<PopupList.Button key={localeOption.code} onClick={close} to={{ search }}>
{localeOption.label}
{localeOption.label !== localeOption.code && ` (${localeOption.code})`}
{localeOptionLabel}
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
</PopupList.Button>
)
})}

View File

@@ -6,9 +6,9 @@ import DatePicker from '../../../DatePicker'
const baseClass = 'condition-value-date'
const DateField: React.FC<Props> = ({ onChange, value }) => (
const DateField: React.FC<Props> = ({ disabled, onChange, value }) => (
<div className={baseClass}>
<DatePicker onChange={onChange} value={value} />
<DatePicker onChange={onChange} readOnly={disabled} value={value} />
</div>
)

View File

@@ -1,4 +1,5 @@
export type Props = {
disabled?: boolean
onChange: () => void
value: Date
}

View File

@@ -7,11 +7,12 @@ import './index.scss'
const baseClass = 'condition-value-number'
const NumberField: React.FC<Props> = ({ onChange, value }) => {
const NumberField: React.FC<Props> = ({ disabled, onChange, value }) => {
const { t } = useTranslation('general')
return (
<input
className={baseClass}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
placeholder={t('enterAValue')}
type="number"

View File

@@ -1,4 +1,5 @@
export type Props = {
disabled?: boolean
onChange: (e: string) => void
value: string
}

View File

@@ -16,7 +16,7 @@ const baseClass = 'condition-value-relationship'
const maxResultsPerRequest = 10
const RelationshipField: React.FC<Props> = (props) => {
const { admin: { isSortable } = {}, hasMany, onChange, relationTo, value } = props
const { admin: { isSortable } = {}, disabled, hasMany, onChange, relationTo, value } = props
const {
collections,
@@ -261,6 +261,7 @@ const RelationshipField: React.FC<Props> = (props) => {
<div className={classes}>
{!errorLoading && (
<ReactSelect
disabled={disabled}
isMulti={hasMany}
isSortable={isSortable}
onChange={(selected) => {

View File

@@ -5,6 +5,7 @@ import type { PaginatedDocs } from '../../../../../../database/types'
import type { RelationshipField } from '../../../../../../fields/config/types'
export type Props = {
disabled?: boolean
onChange: (val: unknown) => void
value: unknown
} & RelationshipField

View File

@@ -20,6 +20,7 @@ const formatOptions = (options: Option[]): OptionObject[] =>
})
export const Select: React.FC<Props> = ({
disabled,
onChange,
operator,
options: optionsFromProps,
@@ -79,6 +80,7 @@ export const Select: React.FC<Props> = ({
return (
<ReactSelect
disabled={disabled}
isMulti={isMulti}
onChange={onSelect}
options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}

View File

@@ -2,6 +2,7 @@ import type { Option } from '../../../../../../fields/config/types'
import type { Operator } from '../../../../../../types'
export type Props = {
disabled?: boolean
onChange: (val: string) => void
operator: Operator
options: Option[]

View File

@@ -7,11 +7,12 @@ import './index.scss'
const baseClass = 'condition-value-text'
const Text: React.FC<Props> = ({ onChange, value }) => {
const Text: React.FC<Props> = ({ disabled, onChange, value }) => {
const { t } = useTranslation('general')
return (
<input
className={baseClass}
disabled={disabled}
onChange={(e) => onChange(e.target.value)}
placeholder={t('enterAValue')}
type="text"

View File

@@ -1,4 +1,5 @@
export type Props = {
disabled?: boolean
onChange: (val: string) => void
value: string
}

View File

@@ -26,25 +26,29 @@ const baseClass = 'condition'
const Condition: React.FC<Props> = (props) => {
const { andIndex, dispatch, fields, orIndex, value } = props
const fieldValue = Object.keys(value)[0]
const operatorAndValue = value?.[fieldValue] ? Object.entries(value[fieldValue])[0] : undefined
const operatorValue = operatorAndValue?.[0]
const queryValue = operatorAndValue?.[1]
const fieldName = Object.keys(value)[0]
const [activeField, setActiveField] = useState<FieldCondition>(() =>
fields.find((field) => fieldValue === field.value),
fields.find((field) => fieldName === field.value),
)
const operatorAndValue = value?.[fieldName] ? Object.entries(value[fieldName])[0] : undefined
const queryValue = operatorAndValue?.[1]
const operatorValue = operatorAndValue?.[0]
const [internalValue, setInternalValue] = useState(queryValue)
const [internalOperatorField, setInternalOperatorField] = useState(operatorValue)
const debouncedValue = useDebounce(internalValue, 300)
useEffect(() => {
const newActiveField = fields.find((field) => fieldValue === field.value)
const newActiveField = fields.find(({ value: name }) => name === fieldName)
if (newActiveField) {
if (newActiveField && newActiveField !== activeField) {
setActiveField(newActiveField)
setInternalOperatorField(null)
setInternalValue('')
}
}, [fieldValue, fields])
}, [fieldName, fields, activeField])
useEffect(() => {
dispatch({
@@ -73,21 +77,23 @@ const Condition: React.FC<Props> = (props) => {
<div className={`${baseClass}__inputs`}>
<div className={`${baseClass}__field`}>
<ReactSelect
onChange={(field) =>
isClearable={false}
onChange={(field) => {
dispatch({
andIndex,
field: field?.value || undefined,
orIndex,
andIndex: andIndex,
field: field?.value,
orIndex: orIndex,
type: 'update',
})
}
}}
options={fields}
value={fields.find((field) => fieldValue === field.value)}
value={fields.find((field) => fieldName === field.value)}
/>
</div>
<div className={`${baseClass}__operator`}>
<ReactSelect
disabled={!fieldValue}
disabled={!fieldName}
isClearable={false}
onChange={(operator) => {
dispatch({
andIndex,
@@ -95,9 +101,14 @@ const Condition: React.FC<Props> = (props) => {
orIndex,
type: 'update',
})
setInternalOperatorField(operator.value)
}}
options={activeField.operators}
value={activeField.operators.find((operator) => operatorValue === operator.value)}
value={
activeField.operators.find(
(operator) => internalOperatorField === operator.value,
) || null
}
/>
</div>
<div className={`${baseClass}__value`}>
@@ -106,6 +117,7 @@ const Condition: React.FC<Props> = (props) => {
DefaultComponent={ValueComponent}
componentProps={{
...activeField?.props,
disabled: !operatorValue,
onChange: setInternalValue,
operator: operatorValue,
options: valueOptions,

View File

@@ -59,17 +59,17 @@ const reducer = (state: Where[], action: Action): Where[] => {
if (field) {
newState[orIndex].and[andIndex] = {
[field]: {
[Object.keys(existingCondition)[0]]: Object.values(existingCondition)[0],
},
[field]: operator ? { [operator]: value } : {},
}
}
if (value !== undefined) {
newState[orIndex].and[andIndex] = {
[existingFieldName]: {
[Object.keys(existingCondition)[0]]: value,
},
[existingFieldName]: Object.keys(existingCondition)[0]
? {
[Object.keys(existingCondition)[0]]: value,
}
: {},
}
}
}

View File

@@ -281,9 +281,9 @@ export const addFieldStatePromise = async ({
return {
relationTo: relationship.relationTo,
value:
typeof relationship.value === 'string'
? relationship.value
: relationship.value?.id,
relationship.value && typeof relationship.value === 'object'
? relationship.value?.id
: relationship.value,
}
}
if (typeof relationship === 'object' && relationship !== null) {

View File

@@ -1,13 +1,18 @@
import React from 'react'
import type { Props as LabelProps } from '../../Label/types'
import Check from '../../../icons/Check'
import Line from '../../../icons/Line'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import './index.scss'
const baseClass = 'checkbox-input'
type CheckboxInputProps = {
AfterInput?: React.ReactElement<any>[]
BeforeInput?: React.ReactElement<any>[]
Label?: React.ComponentType<LabelProps>
'aria-label'?: string
checked?: boolean
className?: string
@@ -25,6 +30,9 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
const {
id,
name,
AfterInput,
BeforeInput,
Label,
'aria-label': ariaLabel,
checked,
className,
@@ -36,6 +44,8 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
required,
} = props
const LabelComp = Label || DefaultLabel
return (
<div
className={[
@@ -48,6 +58,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
.join(' ')}
>
<div className={`${baseClass}__input`}>
{BeforeInput}
<input
aria-label={ariaLabel}
defaultChecked={Boolean(checked)}
@@ -58,12 +69,13 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = (props) => {
ref={inputRef}
type="checkbox"
/>
{AfterInput}
<span className={`${baseClass}__icon ${!partialChecked ? 'check' : 'partial'}`}>
{!partialChecked && <Check />}
{partialChecked && <Line />}
</span>
</div>
{label && <Label htmlFor={id} label={label} required={required} />}
{label && <LabelComp htmlFor={id} label={label} required={required} />}
</div>
)
}

View File

@@ -5,7 +5,7 @@ import type { Props } from './types'
import { checkbox } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import useField from '../../useField'
import withCondition from '../../withCondition'
@@ -18,7 +18,15 @@ const baseClass = 'checkbox'
const Checkbox: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, readOnly, style, width } = {},
admin: {
className,
condition,
description,
readOnly,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
disableFormData,
label,
onChange,
@@ -27,6 +35,8 @@ const Checkbox: React.FC<Props> = (props) => {
validate = checkbox,
} = props
const ErrorComp = Error || DefaultError
const { i18n } = useTranslation()
const path = pathFromProps || name
@@ -72,7 +82,7 @@ const Checkbox: React.FC<Props> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<Error alignCaret="left" message={errorMessage} showError={showError} />
<ErrorComp alignCaret="left" message={errorMessage} showError={showError} />
</div>
<CheckboxInput
checked={Boolean(value)}
@@ -81,6 +91,9 @@ const Checkbox: React.FC<Props> = (props) => {
name={path}
onToggle={onToggle}
readOnly={readOnly}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
required={required}
/>
<FieldDescription description={description} value={value} />

View File

@@ -4,9 +4,9 @@ import type { Props } from './types'
import { code } from '../../../../../fields/validations'
import { CodeEditor } from '../../../elements/CodeEditor'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -31,6 +31,7 @@ const Code: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
path: pathFromProps,
@@ -38,6 +39,9 @@ const Code: React.FC<Props> = (props) => {
validate = code,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
const memoizedValidate = useCallback(
@@ -69,8 +73,8 @@ const Code: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
<CodeEditor
defaultLanguage={prismToMonacoLanguageMap[language] || language}
onChange={readOnly ? () => null : (val) => setValue(val)}

View File

@@ -6,9 +6,9 @@ import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import DatePicker from '../../../elements/DatePicker'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
@@ -16,6 +16,12 @@ const baseClass = 'date-time-field'
export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
className?: string
components: {
AfterInput?: React.ReactElement<any>[]
BeforeInput?: React.ReactElement<any>[]
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
datePickerProps?: DateField['admin']['date']
description?: Description
errorMessage?: string
@@ -33,6 +39,7 @@ export type DateTimeInputProps = Omit<DateField, 'admin' | 'name' | 'type'> & {
export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
const {
className,
components: { AfterInput, BeforeInput, Error, Label } = {},
datePickerProps,
description,
errorMessage,
@@ -48,6 +55,9 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
width,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const { i18n } = useTranslation()
return (
@@ -67,10 +77,11 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<Error message={errorMessage} showError={showError} />
<ErrorComp message={errorMessage} showError={showError} />
</div>
<Label htmlFor={path} label={label} required={required} />
<LabelComp htmlFor={path} label={label} required={required} />
<div className={`${baseClass}__input-wrapper`} id={`field-${path.replace(/\./g, '__')}`}>
{BeforeInput}
<DatePicker
{...datePickerProps}
onChange={onChange}
@@ -78,6 +89,7 @@ export const DateTimeInput: React.FC<DateTimeInputProps> = (props) => {
readOnly={readOnly}
value={value}
/>
{AfterInput}
</div>
<FieldDescription description={description} value={value} />
</div>

View File

@@ -11,7 +11,17 @@ import './index.scss'
const DateTime: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, date, description, placeholder, readOnly, style, width } = {},
admin: {
className,
components,
condition,
date,
description,
placeholder,
readOnly,
style,
width,
} = {},
label,
path: pathFromProps,
required,
@@ -36,6 +46,7 @@ const DateTime: React.FC<Props> = (props) => {
return (
<DateTimeInput
className={className}
components={components}
datePickerProps={date}
description={description}
errorMessage={errorMessage}

View File

@@ -5,9 +5,9 @@ import type { Props } from './types'
import { email } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -25,6 +25,7 @@ const Email: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
path: pathFromProps,
@@ -51,6 +52,9 @@ const Email: React.FC<Props> = (props) => {
const { errorMessage, setValue, showError, value } = fieldType
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
return (
<div
className={[fieldBaseClass, 'email', className, showError && 'error', readOnly && 'read-only']
@@ -61,18 +65,22 @@ const Email: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<input
autoComplete={autoComplete}
disabled={Boolean(readOnly)}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
type="email"
value={(value as string) || ''}
/>
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{BeforeInput}
<input
autoComplete={autoComplete}
disabled={Boolean(readOnly)}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
type="email"
value={(value as string) || ''}
/>
{AfterInput}
</div>
<FieldDescription description={description} value={value} />
</div>
)

View File

@@ -4,9 +4,9 @@ import type { Props } from './types'
import { json } from '../../../../../fields/validations'
import { CodeEditor } from '../../../elements/CodeEditor'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -17,13 +17,25 @@ const baseClass = 'json-field'
const JSONField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, editorOptions, readOnly, style, width } = {},
admin: {
className,
condition,
description,
editorOptions,
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
path: pathFromProps,
required,
validate = json,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
const [stringValue, setStringValue] = useState<string>()
const [jsonError, setJsonError] = useState<string>()
@@ -76,8 +88,8 @@ const JSONField: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
<CodeEditor
defaultLanguage="json"
onChange={handleChange}

View File

@@ -8,9 +8,9 @@ import { number } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import { isNumber } from '../../../../../utilities/isNumber'
import ReactSelect from '../../../elements/ReactSelect'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -19,7 +19,17 @@ import { fieldBaseClass } from '../shared'
const NumberField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
admin: {
className,
condition,
description,
placeholder,
readOnly,
step,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
hasMany,
label,
max,
@@ -31,6 +41,9 @@ const NumberField: React.FC<Props> = (props) => {
validate = number,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const { i18n, t } = useTranslation()
const path = pathFromProps || name
@@ -118,8 +131,8 @@ const NumberField: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
@@ -148,21 +161,25 @@ const NumberField: React.FC<Props> = (props) => {
value={valueToRender as Option[]}
/>
) : (
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={typeof value === 'number' ? value : ''}
/>
<div className="input-wrapper">
{BeforeInput}
<input
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={handleChange}
onWheel={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
e.target.blur()
}}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={typeof value === 'number' ? value : ''}
/>
{AfterInput}
</div>
)}
<FieldDescription description={description} value={value} />

View File

@@ -5,9 +5,9 @@ import type { Props } from './types'
import { point } from '../../../../../fields/validations'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import './index.scss'
@@ -18,13 +18,26 @@ const baseClass = 'point'
const PointField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, step, style, width } = {},
admin: {
className,
condition,
description,
placeholder,
readOnly,
step,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
path: pathFromProps,
required,
validate = point,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
const { i18n, t } = useTranslation('fields')
@@ -76,41 +89,49 @@ const PointField: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<ErrorComp message={errorMessage} showError={showError} />
<ul className={`${baseClass}__wrap`}>
<li>
<Label
<LabelComp
htmlFor={`field-longitude-${path.replace(/\./g, '__')}`}
label={`${getTranslation(label || name, i18n)} - ${t('longitude')}`}
required={required}
/>
<input
disabled={readOnly}
id={`field-longitude-${path.replace(/\./g, '__')}`}
name={`${path}.longitude`}
onChange={(e) => handleChange(e, 0)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[0] === 'number' ? value[0] : ''}
/>
<div className="input-wrapper">
{BeforeInput}
<input
disabled={readOnly}
id={`field-longitude-${path.replace(/\./g, '__')}`}
name={`${path}.longitude`}
onChange={(e) => handleChange(e, 0)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[0] === 'number' ? value[0] : ''}
/>
{AfterInput}
</div>
</li>
<li>
<Label
<LabelComp
htmlFor={`field-latitude-${path.replace(/\./g, '__')}`}
label={`${getTranslation(label || name, i18n)} - ${t('latitude')}`}
required={required}
/>
<input
disabled={readOnly}
id={`field-latitude-${path.replace(/\./g, '__')}`}
name={`${path}.latitude`}
onChange={(e) => handleChange(e, 1)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[1] === 'number' ? value[1] : ''}
/>
<div className="input-wrapper">
{BeforeInput}
<input
disabled={readOnly}
id={`field-latitude-${path.replace(/\./g, '__')}`}
name={`${path}.latitude`}
onChange={(e) => handleChange(e, 1)}
placeholder={getTranslation(placeholder, i18n)}
step={step}
type="number"
value={value && typeof value[1] === 'number' ? value[1] : ''}
/>
{AfterInput}
</div>
</li>
</ul>
<FieldDescription description={description} value={value} />

View File

@@ -5,9 +5,9 @@ import type { Description } from '../../FieldDescription/types'
import type { OnChange } from './types'
import { optionIsObject } from '../../../../../fields/config/types'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import RadioInput from './RadioInput'
import './index.scss'
import { fieldBaseClass } from '../shared'
@@ -28,6 +28,8 @@ export type RadioGroupInputProps = Omit<RadioField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
@@ -47,8 +49,13 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const path = pathFromProps || name
return (
@@ -69,9 +76,9 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
}}
>
<div className={`${baseClass}__error-wrap`}>
<Error message={errorMessage} showError={showError} />
<ErrorComp message={errorMessage} showError={showError} />
</div>
<Label htmlFor={`field-${path}`} label={label} required={required} />
<LabelComp htmlFor={`field-${path}`} label={label} required={required} />
<ul className={`${baseClass}--group`} id={`field-${path.replace(/\./g, '__')}`}>
{options.map((option) => {
let optionValue = ''

View File

@@ -18,6 +18,7 @@ const RadioGroup: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
label,
options,
@@ -57,6 +58,8 @@ const RadioGroup: React.FC<Props> = (props) => {
style={style}
value={value}
width={width}
Error={Error}
Label={Label}
/>
)
}

View File

@@ -15,12 +15,13 @@ import { useAuth } from '../../../utilities/Auth'
import { useConfig } from '../../../utilities/Config'
import { GetFilterOptions } from '../../../utilities/GetFilterOptions'
import { useLocale } from '../../../utilities/Locale'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import { useFormProcessing } from '../../Form/context'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import useField from '../../useField'
import withCondition from '../../withCondition'
import { fieldBaseClass } from '../shared'
import { AddNewRelation } from './AddNew'
import { createRelationMap } from './createRelationMap'
import { findOptionsByValue } from './findOptionsByValue'
@@ -28,7 +29,6 @@ import './index.scss'
import optionsReducer from './optionsReducer'
import { MultiValueLabel } from './select-components/MultiValueLabel'
import { SingleValue } from './select-components/SingleValue'
import { fieldBaseClass } from '../shared'
const maxResultsPerRequest = 10
@@ -46,6 +46,7 @@ const Relationship: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
filterOptions,
hasMany,
@@ -56,6 +57,9 @@ const Relationship: React.FC<Props> = (props) => {
validate = relationship,
} = props
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const config = useConfig()
const {
@@ -391,6 +395,7 @@ const Relationship: React.FC<Props> = (props) => {
}, [])
const valueToRender = findOptionsByValue({ options, value })
if (!Array.isArray(valueToRender) && valueToRender?.value === 'null') valueToRender.value = null
return (
@@ -411,8 +416,8 @@ const Relationship: React.FC<Props> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={pathOrName} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={pathOrName} label={label} required={required} />
<GetFilterOptions
{...{
filterOptions,

View File

@@ -7,9 +7,9 @@ import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import ReactSelect from '../../../elements/ReactSelect'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
@@ -29,6 +29,8 @@ export type SelectInputProps = Omit<SelectField, 'options' | 'type' | 'value'> &
style?: React.CSSProperties
value?: string | string[]
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const SelectInput: React.FC<SelectInputProps> = (props) => {
@@ -50,10 +52,15 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
let valueToRender = defaultValue
if (hasMany && Array.isArray(value)) {
@@ -89,8 +96,8 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ReactSelect
disabled={readOnly}
isClearable={isClearable}

View File

@@ -32,6 +32,7 @@ const Select: React.FC<Props> = (props) => {
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
hasMany,
label,
@@ -103,6 +104,8 @@ const Select: React.FC<Props> = (props) => {
style={style}
value={value as string | string[]}
width={width}
Error={Error}
Label={Label}
/>
)
}

View File

@@ -7,9 +7,9 @@ import type { TextField } from '../../../../../fields/config/types'
import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
@@ -29,6 +29,10 @@ export type TextInputProps = Omit<TextField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
const TextInput: React.FC<TextInputProps> = (props) => {
@@ -49,10 +53,17 @@ const TextInput: React.FC<TextInputProps> = (props) => {
style,
value,
width,
Error,
Label,
BeforeInput,
AfterInput,
} = props
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
return (
<div
className={[fieldBaseClass, 'text', className, showError && 'error', readOnly && 'read-only']
@@ -63,20 +74,24 @@ const TextInput: React.FC<TextInputProps> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<div className="input-wrapper">
{BeforeInput}
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
{AfterInput}
</div>
<FieldDescription
className={`field-description-${path.replace(/\./g, '__')}`}
description={description}

View File

@@ -13,7 +13,17 @@ import TextInput from './Input'
const Text: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, placeholder, readOnly, rtl, style, width } = {},
admin: {
className,
condition,
description,
placeholder,
readOnly,
rtl,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
inputRef,
label,
localized,
@@ -68,6 +78,10 @@ const Text: React.FC<Props> = (props) => {
style={style}
value={value}
width={width}
Error={Error}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
/>
)
}

View File

@@ -7,9 +7,9 @@ import type { TextareaField } from '../../../../../fields/config/types'
import type { Description } from '../../FieldDescription/types'
import { getTranslation } from '../../../../../utilities/getTranslation'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import './index.scss'
import { fieldBaseClass } from '../shared'
@@ -28,6 +28,10 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
@@ -47,10 +51,17 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
style,
value,
width,
Error,
Label,
BeforeInput,
AfterInput,
} = props
const { i18n } = useTranslation()
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
return (
<div
className={[
@@ -67,11 +78,12 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
width,
}}
>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
{BeforeInput}
<textarea
className="textarea-element"
data-rtl={rtl}
@@ -83,6 +95,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
rows={rows}
value={value || ''}
/>
{AfterInput}
</div>
</label>
<FieldDescription description={description} value={value} />

View File

@@ -26,6 +26,7 @@ const Textarea: React.FC<Props> = (props) => {
rtl,
style,
width,
components: { Error, Label, BeforeInput, AfterInput } = {},
} = {},
label,
localized,
@@ -82,6 +83,10 @@ const Textarea: React.FC<Props> = (props) => {
style={style}
value={value as string}
width={width}
Error={Error}
Label={Label}
BeforeInput={BeforeInput}
AfterInput={AfterInput}
/>
)
}

View File

@@ -15,9 +15,9 @@ import { useDocumentDrawer } from '../../../elements/DocumentDrawer'
import FileDetails from '../../../elements/FileDetails'
import { useListDrawer } from '../../../elements/ListDrawer'
import { GetFilterOptions } from '../../../utilities/GetFilterOptions'
import Error from '../../Error'
import DefaultError from '../../Error'
import FieldDescription from '../../FieldDescription'
import Label from '../../Label'
import DefaultLabel from '../../Label'
import { fieldBaseClass } from '../shared'
import './index.scss'
@@ -41,6 +41,8 @@ export type UploadInputProps = Omit<UploadField, 'type'> & {
style?: React.CSSProperties
value?: string
width?: string
Error?: React.ComponentType<any>
Label?: React.ComponentType<any>
}
const UploadInput: React.FC<UploadInputProps> = (props) => {
@@ -62,10 +64,15 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
style,
value,
width,
Error,
Label,
} = props
const { i18n, t } = useTranslation('fields')
const ErrorComp = Error || DefaultError
const LabelComp = Label || DefaultLabel
const [file, setFile] = useState(undefined)
const [missingFile, setMissingFile] = useState(false)
const [collectionSlugs] = useState([collection?.slug])
@@ -149,8 +156,8 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
setFilterOptionsResult,
}}
/>
<Error message={errorMessage} showError={showError} />
<Label htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
<ErrorComp message={errorMessage} showError={showError} />
<LabelComp htmlFor={`field-${path.replace(/\./g, '__')}`} label={label} required={required} />
{collection?.upload && (
<React.Fragment>
{file && !missingFile && (

View File

@@ -18,7 +18,15 @@ const Upload: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, readOnly, style, width } = {},
admin: {
className,
condition,
description,
readOnly,
style,
width,
components: { Error, Label } = {},
} = {},
fieldTypes,
filterOptions,
label,
@@ -75,6 +83,8 @@ const Upload: React.FC<Props> = (props) => {
style={style}
value={value as string}
width={width}
Error={Error}
Label={Label}
/>
)
}

View File

@@ -293,6 +293,4 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
)
}
type UseAuth<T = User> = () => AuthContext<T>
export const useAuth: UseAuth = () => useContext(Context)
export const useAuth = <T = User,>(): AuthContext<T> => useContext(Context) as AuthContext<T>

View File

@@ -1,3 +1,4 @@
import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload'
import formatName from '../../../graphql/utilities/formatName'
@@ -18,7 +19,7 @@ const formatConfigNames = (results, configs) => {
function accessResolver(payload: Payload) {
async function resolver(_, args, context) {
const options = {
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const accessResults = await access(options)

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import forgotPassword from '../../operations/forgotPassword'
@@ -11,7 +12,7 @@ function forgotPasswordResolver(collection: Collection): any {
},
disableEmail: args.disableEmail,
expiration: args.expiration,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
await forgotPassword(options)

View File

@@ -1,10 +1,12 @@
import type { PayloadRequest } from '../../../express/types'
import init from '../../operations/init'
function initResolver(collection: string) {
async function resolver(_, args, context) {
const options = {
collection,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
return init(options)

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import login from '../../operations/login'
@@ -11,7 +12,7 @@ function loginResolver(collection: Collection) {
password: args.password,
},
depth: 0,
req: context.req,
req: { ...context.req } as PayloadRequest,
res: context.res,
}

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import logout from '../../operations/logout'
@@ -6,7 +7,7 @@ function logoutResolver(collection: Collection): any {
async function resolver(_, args, context) {
const options = {
collection,
req: context.req,
req: { ...context.req } as PayloadRequest,
res: context.res,
}

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import me from '../../operations/me'
@@ -7,10 +8,11 @@ function meResolver(collection: Collection): any {
const options = {
collection,
depth: 0,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
return me(options)
}
return resolver
}

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import getExtractJWT from '../../getExtractJWT'
import refresh from '../../operations/refresh'
@@ -17,7 +18,7 @@ function refreshResolver(collection: Collection) {
const options = {
collection,
depth: 0,
req: context.req,
req: { ...context.req } as PayloadRequest,
res: context.res,
token,
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import resetPassword from '../../operations/resetPassword'
@@ -13,7 +14,7 @@ function resetPasswordResolver(collection: Collection) {
collection,
data: args,
depth: 0,
req: context.req,
req: { ...context.req } as PayloadRequest,
res: context.res,
}

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import unlock from '../../operations/unlock'
@@ -7,12 +8,13 @@ function unlockResolver(collection: Collection) {
const options = {
collection,
data: { email: args.email },
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await unlock(options)
return result
}
return resolver
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import verifyEmail from '../../operations/verifyEmail'
@@ -11,7 +12,7 @@ function verifyEmailResolver(collection: Collection) {
const options = {
api: 'GraphQL',
collection,
req: context.req,
req: { ...context.req } as PayloadRequest,
res: context.res,
token: args.token,
}

View File

@@ -37,7 +37,7 @@ export default function createResolver<TSlug extends keyof GeneratedTypes['colle
data: args.data,
depth: 0,
draft: args.draft,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await create(options)

View File

@@ -30,7 +30,7 @@ export default function getDeleteResolver<TSlug extends keyof GeneratedTypes['co
id: args.id,
collection,
depth: 0,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await deleteByID(options)

View File

@@ -18,7 +18,7 @@ export function docAccessResolver(): Resolver {
async function resolver(_, args, context) {
return docAccess({
id: args.id,
req: context.req,
req: { ...context.req } as PayloadRequest,
})
}

View File

@@ -36,7 +36,7 @@ export default function findResolver(collection: Collection): Resolver {
draft: args.draft,
limit: args.limit,
page: args.page,
req: context.req,
req: { ...context.req } as PayloadRequest,
sort: args.sort,
where: args.where,
}

View File

@@ -31,7 +31,7 @@ export default function findVersionByIDResolver(collection: Collection): Resolve
collection,
depth: 0,
draft: args.draft,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await findVersionByID(options)

View File

@@ -35,7 +35,7 @@ export default function findVersionsResolver(collection: Collection): Resolver {
depth: 0,
limit: args.limit,
page: args.page,
req: context.req,
req: { ...context.req } as PayloadRequest,
sort: args.sort,
where: args.where,
}

View File

@@ -23,7 +23,7 @@ export default function restoreVersionResolver(collection: Collection): Resolver
id: args.id,
collection,
depth: 0,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await restoreVersion(options)

View File

@@ -36,7 +36,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['colle
data: args.data,
depth: 0,
draft: args.draft,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await updateByID<TSlug>(options)

View File

@@ -134,7 +134,13 @@ export default joi.object({
joi.array().items(
joi.object().keys({
code: joi.string(),
label: joi.string(),
label: joi
.alternatives()
.try(
joi.object().pattern(joi.string(), [joi.string()]),
joi.string(),
joi.valid(false),
),
rtl: joi.boolean(),
toString: joi.func(),
}),

View File

@@ -288,7 +288,7 @@ export type Locale = {
* label of supported locale
* @example "English"
*/
label: string
label: string | Record<string, string>
/**
* if true, defaults textAligmnent on text fields to RTL
*/

View File

@@ -1,4 +1,5 @@
export { useStepNav } from '../../admin/components/elements/StepNav'
export { useTableColumns } from '../../admin/components/elements/TableColumns'
export { default as useDebounce } from '../../admin/hooks/useDebounce'
export { useDebouncedCallback } from '../../admin/hooks/useDebouncedCallback'
export { useDelay } from '../../admin/hooks/useDelay'

View File

@@ -75,6 +75,12 @@ export const text = baseField.keys({
.alternatives()
.try(joi.object().pattern(joi.string(), [joi.string()]), joi.string()),
rtl: joi.boolean(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
maxLength: joi.number(),
@@ -88,6 +94,18 @@ export const number = baseField.keys({
autoComplete: joi.string(),
placeholder: joi.string(),
step: joi.number(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi
.array()
.items(componentSchema)
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
AfterInput: joi
.array()
.items(componentSchema)
.when('hasMany', { not: true, otherwise: joi.forbidden() }),
}),
}),
defaultValue: joi.alternatives().try(joi.number(), joi.func()),
hasMany: joi.boolean().default(false),
@@ -104,6 +122,12 @@ export const textarea = baseField.keys({
placeholder: joi.string(),
rows: joi.number(),
rtl: joi.boolean(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
maxLength: joi.number(),
@@ -116,6 +140,12 @@ export const email = baseField.keys({
admin: baseAdminFields.keys({
autoComplete: joi.string(),
placeholder: joi.string(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
maxLength: joi.number(),
@@ -128,6 +158,10 @@ export const code = baseField.keys({
admin: baseAdminFields.keys({
editorOptions: joi.object().unknown(), // Editor['options'] @monaco-editor/react
language: joi.string(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
type: joi.string().valid('code').required(),
@@ -135,6 +169,12 @@ export const code = baseField.keys({
export const json = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.array(), joi.object()),
type: joi.string().valid('json').required(),
})
@@ -144,6 +184,10 @@ export const select = baseField.keys({
admin: baseAdminFields.keys({
isClearable: joi.boolean().default(false),
isSortable: joi.boolean().default(false),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi
.alternatives()
@@ -171,6 +215,10 @@ export const radio = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
layout: joi.string().valid('vertical', 'horizontal'),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.string().allow(''), joi.func()),
options: joi
@@ -268,6 +316,12 @@ export const array = baseField.keys({
export const upload = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.object(), joi.func()),
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
maxDepth: joi.number(),
@@ -277,12 +331,28 @@ export const upload = baseField.keys({
export const checkbox = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.boolean(), joi.func()),
type: joi.string().valid('checkbox').required(),
})
export const point = baseField.keys({
name: joi.string().required(),
admin: baseAdminFields.keys({
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.array().items(joi.number()).max(2).min(2), joi.func()),
type: joi.string().valid('point').required(),
})
@@ -292,6 +362,10 @@ export const relationship = baseField.keys({
admin: baseAdminFields.keys({
allowCreate: joi.boolean().default(true),
isSortable: joi.boolean().default(false),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
}),
}),
defaultValue: joi.alternatives().try(joi.func()),
filterOptions: joi.alternatives().try(joi.object(), joi.func()),
@@ -382,6 +456,12 @@ export const date = baseField.keys({
timeIntervals: joi.number(),
}),
placeholder: joi.string(),
components: baseAdminComponentFields.keys({
Label: componentSchema,
Error: componentSchema,
BeforeInput: joi.array().items(componentSchema),
AfterInput: joi.array().items(componentSchema),
}),
}),
defaultValue: joi.alternatives().try(joi.string(), joi.func()),
type: joi.string().valid('date').required(),

View File

@@ -15,6 +15,8 @@ import type { PayloadRequest, RequestContext } from '../../express/types'
import type { SanitizedGlobalConfig } from '../../globals/config/types'
import type { Payload } from '../../payload'
import type { Operation, Where } from '../../types'
import type { Props as ErrorProps } from '../../admin/components/forms/Error/types'
import type { Props as LabelProps } from '../../admin/components/forms/Label/types'
export type FieldHookArgs<T extends TypeWithID = any, P = any, S = any> = {
/** The collection which the field belongs to. If the field belongs to a global, this will be null. */
@@ -158,6 +160,12 @@ export type NumberField = FieldBase & {
placeholder?: Record<string, string> | string
/** Set a value for the number field to increment / decrement using browser controls. */
step?: number
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
}
/** Maximum value accepted. Used in the default `validation` function. */
max?: number
@@ -188,6 +196,12 @@ export type TextField = FieldBase & {
autoComplete?: string
placeholder?: Record<string, string> | string
rtl?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
}
maxLength?: number
minLength?: number
@@ -198,6 +212,12 @@ export type EmailField = FieldBase & {
admin?: Admin & {
autoComplete?: string
placeholder?: Record<string, string> | string
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
}
type: 'email'
}
@@ -207,6 +227,12 @@ export type TextareaField = FieldBase & {
placeholder?: Record<string, string> | string
rows?: number
rtl?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
}
maxLength?: number
minLength?: number
@@ -215,12 +241,26 @@ export type TextareaField = FieldBase & {
export type CheckboxField = FieldBase & {
type: 'checkbox'
admin?: Admin & {
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
}
}
export type DateField = FieldBase & {
admin?: Admin & {
date?: ConditionalDateProps
placeholder?: Record<string, string> | string
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
BeforeInput?: React.ReactElement<any>[]
AfterInput?: React.ReactElement<any>[]
}
}
type: 'date'
}
@@ -315,6 +355,12 @@ export type UIField = {
}
export type UploadField = FieldBase & {
admin?: {
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
}
filterOptions?: FilterOptions
maxDepth?: number
relationTo: string
@@ -324,6 +370,10 @@ export type UploadField = FieldBase & {
type CodeAdmin = Admin & {
editorOptions?: EditorProps['options']
language?: string
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
}
export type CodeField = Omit<FieldBase, 'admin'> & {
@@ -335,6 +385,10 @@ export type CodeField = Omit<FieldBase, 'admin'> & {
type JSONAdmin = Admin & {
editorOptions?: EditorProps['options']
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
}
export type JSONField = Omit<FieldBase, 'admin'> & {
@@ -346,6 +400,10 @@ export type SelectField = FieldBase & {
admin?: Admin & {
isClearable?: boolean
isSortable?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
}
hasMany?: boolean
options: Option[]
@@ -356,6 +414,10 @@ export type RelationshipField = FieldBase & {
admin?: Admin & {
allowCreate?: boolean
isSortable?: boolean
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
}
filterOptions?: FilterOptions
hasMany?: boolean
@@ -440,6 +502,10 @@ export type ArrayField = FieldBase & {
export type RadioField = FieldBase & {
admin?: Admin & {
layout?: 'horizontal' | 'vertical'
components?: {
Error?: React.ComponentType<ErrorProps>
Label?: React.ComponentType<LabelProps>
}
}
options: Option[]
type: 'radio'

View File

@@ -16,7 +16,7 @@ export function docAccessResolver(global: SanitizedGlobalConfig): Resolver {
async function resolver(_, context) {
return docAccess({
globalConfig: global,
req: context.req,
req: { ...context.req } as PayloadRequest,
})
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */
import type { Document } from '../../../types'
import type { Document, PayloadRequest } from '../../../types'
import type { SanitizedGlobalConfig } from '../../config/types'
import findOne from '../../operations/findOne'
@@ -16,7 +16,7 @@ export default function findOneResolver(globalConfig: SanitizedGlobalConfig): Do
depth: 0,
draft: args.draft,
globalConfig,
req: context.req,
req: { ...context.req } as PayloadRequest,
slug,
}

View File

@@ -31,7 +31,7 @@ export default function findVersionByIDResolver(globalConfig: SanitizedGlobalCon
depth: 0,
draft: args.draft,
globalConfig,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await findVersionByID(options)

View File

@@ -29,7 +29,7 @@ export default function findVersionsResolver(globalConfig: SanitizedGlobalConfig
globalConfig,
limit: args.limit,
page: args.page,
req: context.req,
req: { ...context.req } as PayloadRequest,
sort: args.sort,
where: args.where,
}

View File

@@ -22,7 +22,7 @@ export default function restoreVersionResolver(globalConfig: SanitizedGlobalConf
id: args.id,
depth: 0,
globalConfig,
req: context.req,
req: { ...context.req } as PayloadRequest,
}
const result = await restoreVersion(options)

View File

@@ -35,7 +35,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['globa
depth: 0,
draft: args.draft,
globalConfig,
req: context.req,
req: { ...context.req } as PayloadRequest,
slug,
}

View File

@@ -17,12 +17,27 @@ export const fieldSchemaToJSON = (fields: Field[]): FieldSchemaJSON => {
switch (field.type) {
case 'group':
case 'array':
acc.push({
name: field.name,
fields: fieldSchemaToJSON(field.fields),
type: field.type,
})
break
case 'array':
acc.push({
name: field.name,
fields: fieldSchemaToJSON([
...field.fields,
{
name: 'id',
type: 'text',
},
]),
type: field.type,
})
break
case 'blocks':
@@ -30,19 +45,25 @@ export const fieldSchemaToJSON = (fields: Field[]): FieldSchemaJSON => {
name: field.name,
blocks: field.blocks.reduce((acc, block) => {
acc[block.slug] = {
fields: fieldSchemaToJSON(block.fields),
fields: fieldSchemaToJSON([
...block.fields,
{
name: 'id',
type: 'text',
},
]),
}
return acc
}, {}),
type: field.type,
})
break
case 'row':
case 'collapsible':
result = result.concat(fieldSchemaToJSON(field.fields))
break
case 'tabs': {

View File

@@ -85,7 +85,22 @@ export default buildConfigWithDefaults({
},
localization: {
defaultLocale: 'en',
locales: ['en', 'es'],
locales: [
{
label: {
es: 'Español',
en: 'Spanish',
},
code: 'es',
},
{
label: {
es: 'Inglés',
en: 'English',
},
code: 'en',
},
],
},
collections: [
Posts,

View File

@@ -488,6 +488,43 @@ describe('admin', () => {
'Home',
)
})
test('should allow custom translation of locale labels', async () => {
const selectOptionClass = '.localizer .popup-button-list__button'
const localizorButton = page.locator('.localizer .popup-button')
const secondLocale = page.locator(selectOptionClass).nth(1)
async function checkLocalLabels(firstLabel: string, secondLabel: string) {
await localizorButton.click()
await expect(page.locator(selectOptionClass).first()).toContainText(firstLabel)
await expect(page.locator(selectOptionClass).nth(1)).toContainText(secondLabel)
}
await checkLocalLabels('English (en)', 'Spanish (es)')
// Change locale to Spanish
await localizorButton.click()
await expect(secondLocale).toContainText('Spanish (es)')
await secondLocale.click()
// Go to account page
await page.goto(url.account)
const languageField = page.locator('.payload-settings__language .react-select')
const options = page.locator('.rs__option')
// Change language to Spanish
await languageField.click()
await options.locator('text=Español').click()
await checkLocalLabels('Inglés (en)', 'Español (es)')
// Change locale and language back to English
await languageField.click()
await options.locator('text=English').click()
await localizorButton.click()
await expect(secondLocale).toContainText('Spanish (es)')
})
})
describe('list view', () => {
@@ -647,6 +684,38 @@ describe('admin', () => {
await expect(page.locator(tableRowLocator)).toHaveCount(2)
})
test('resets filter value and operator on field update', async () => {
const { id } = await createPost({ title: 'post1' })
await createPost({ title: 'post2' })
// open the column controls
await page.locator('.list-controls__toggle-columns').click()
await page.locator('.list-controls__toggle-where').click()
await page.waitForSelector('.list-controls__where.rah-static--height-auto')
await page.locator('.where-builder__add-first-filter').click()
const operatorField = page.locator('.condition__operator')
await operatorField.click()
const dropdownOperatorOptions = operatorField.locator('.rs__option')
await dropdownOperatorOptions.locator('text=equals').click()
// execute filter (where ID equals id value)
const valueField = page.locator('.condition__value > input')
await valueField.fill(id)
const filterField = page.locator('.condition__field')
await filterField.click()
// select new filter field of Number
const dropdownFieldOptions = filterField.locator('.rs__option')
await dropdownFieldOptions.locator('text=Number').click()
// expect operator & value field to reset (be empty)
await expect(operatorField.locator('.rs__placeholder')).toContainText('Select a value')
await expect(valueField).toHaveValue('')
})
test('should accept where query from valid URL where parameter', async () => {
await createPost({ title: 'post1' })
await createPost({ title: 'post2' })

View File

@@ -12,14 +12,13 @@ export interface Relation {
const openAccess = {
create: () => true,
delete: () => true,
read: () => true,
update: () => true,
delete: () => true,
}
const collectionWithName = (collectionSlug: string): CollectionConfig => {
return {
slug: collectionSlug,
access: openAccess,
fields: [
{
@@ -27,6 +26,7 @@ const collectionWithName = (collectionSlug: string): CollectionConfig => {
type: 'text',
},
],
slug: collectionSlug,
}
}
@@ -35,47 +35,27 @@ export const relationSlug = 'relation'
export const pointSlug = 'point'
export const errorOnHookSlug = 'error-on-hooks'
export default buildConfigWithDefaults({
graphQL: {
schemaOutputFile: path.resolve(__dirname, 'schema.graphql'),
queries: (GraphQL) => {
return {
QueryWithInternalError: {
type: new GraphQL.GraphQLObjectType({
name: 'QueryWithInternalError',
fields: {
text: {
type: GraphQL.GraphQLString,
},
},
}),
resolve: () => {
// Throwing an internal error with potentially sensitive data
throw new Error('Lost connection to the Pentagon. Secret data: ******')
},
},
}
},
},
collections: [
{
slug: 'users',
auth: true,
access: openAccess,
auth: true,
fields: [],
slug: 'users',
},
{
slug: pointSlug,
access: openAccess,
fields: [
{
type: 'point',
name: 'point',
type: 'point',
},
],
slug: pointSlug,
},
{
slug,
access: openAccess,
fields: [
{
@@ -92,173 +72,173 @@ export default buildConfigWithDefaults({
},
{
name: 'min',
type: 'number',
min: 10,
type: 'number',
},
// Relationship
{
name: 'relationField',
type: 'relationship',
relationTo: relationSlug,
type: 'relationship',
},
{
name: 'relationToCustomID',
type: 'relationship',
relationTo: 'custom-ids',
type: 'relationship',
},
// Relation hasMany
{
name: 'relationHasManyField',
type: 'relationship',
relationTo: relationSlug,
hasMany: true,
relationTo: relationSlug,
type: 'relationship',
},
// Relation multiple relationTo
{
name: 'relationMultiRelationTo',
type: 'relationship',
relationTo: [relationSlug, 'dummy'],
type: 'relationship',
},
// Relation multiple relationTo hasMany
{
name: 'relationMultiRelationToHasMany',
type: 'relationship',
relationTo: [relationSlug, 'dummy'],
hasMany: true,
relationTo: [relationSlug, 'dummy'],
type: 'relationship',
},
{
name: 'A1',
type: 'group',
fields: [
{
type: 'text',
name: 'A2',
defaultValue: 'textInRowInGroup',
type: 'text',
},
],
type: 'group',
},
{
name: 'B1',
type: 'group',
fields: [
{
type: 'collapsible',
label: 'Collapsible',
fields: [
{
type: 'text',
name: 'B2',
defaultValue: 'textInRowInGroup',
type: 'text',
},
],
label: 'Collapsible',
type: 'collapsible',
},
],
type: 'group',
},
{
name: 'C1',
type: 'group',
fields: [
{
type: 'text',
name: 'C2Text',
type: 'text',
},
{
type: 'row',
fields: [
{
type: 'collapsible',
label: 'Collapsible2',
fields: [
{
name: 'C2',
type: 'group',
fields: [
{
type: 'row',
fields: [
{
type: 'collapsible',
label: 'Collapsible2',
fields: [
{
type: 'text',
name: 'C3',
type: 'text',
},
],
label: 'Collapsible2',
type: 'collapsible',
},
],
type: 'row',
},
],
type: 'group',
},
],
label: 'Collapsible2',
type: 'collapsible',
},
],
type: 'row',
},
],
type: 'group',
},
{
type: 'tabs',
tabs: [
{
label: 'Tab1',
name: 'D1',
fields: [
{
name: 'D2',
type: 'group',
fields: [
{
type: 'row',
fields: [
{
type: 'collapsible',
label: 'Collapsible2',
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Tab1',
fields: [
{
name: 'D3',
type: 'group',
fields: [
{
type: 'row',
fields: [
{
type: 'collapsible',
label: 'Collapsible2',
fields: [
{
type: 'text',
name: 'D4',
type: 'text',
},
],
label: 'Collapsible2',
type: 'collapsible',
},
],
type: 'row',
},
],
type: 'group',
},
],
label: 'Tab1',
},
],
type: 'tabs',
},
],
label: 'Collapsible2',
type: 'collapsible',
},
],
type: 'row',
},
],
type: 'group',
},
],
label: 'Tab1',
},
],
type: 'tabs',
},
],
slug,
},
{
slug: 'custom-ids',
access: {
read: () => true,
},
@@ -272,47 +252,98 @@ export default buildConfigWithDefaults({
type: 'text',
},
],
slug: 'custom-ids',
},
collectionWithName(relationSlug),
collectionWithName('dummy'),
{
slug: 'payload-api-test-ones',
access: {
read: () => true,
},
...collectionWithName(errorOnHookSlug),
fields: [
{
name: 'payloadAPI',
name: 'title',
type: 'text',
hooks: {
afterRead: [({ req }) => req.payloadAPI],
},
},
{
name: 'errorBeforeChange',
type: 'checkbox',
},
],
hooks: {
afterDelete: [
({ doc }) => {
if (doc?.errorAfterDelete) {
throw new Error('Error After Delete Thrown')
}
},
],
beforeChange: [
({ originalDoc }) => {
if (originalDoc?.errorBeforeChange) {
throw new Error('Error Before Change Thrown')
}
},
],
},
},
{
slug: 'payload-api-test-twos',
access: {
read: () => true,
},
fields: [
{
name: 'payloadAPI',
type: 'text',
hooks: {
afterRead: [({ req }) => req.payloadAPI],
},
type: 'text',
},
],
slug: 'payload-api-test-ones',
},
{
access: {
read: () => true,
},
fields: [
{
name: 'payloadAPI',
hooks: {
afterRead: [({ req }) => req.payloadAPI],
},
type: 'text',
},
{
name: 'relation',
type: 'relationship',
relationTo: 'payload-api-test-ones',
type: 'relationship',
},
],
slug: 'payload-api-test-twos',
},
],
graphQL: {
queries: (GraphQL) => {
return {
QueryWithInternalError: {
resolve: () => {
// Throwing an internal error with potentially sensitive data
throw new Error('Lost connection to the Pentagon. Secret data: ******')
},
type: new GraphQL.GraphQLObjectType({
name: 'QueryWithInternalError',
fields: {
text: {
type: GraphQL.GraphQLString,
},
},
}),
},
}
},
schemaOutputFile: path.resolve(__dirname, 'schema.graphql'),
},
onInit: async (payload) => {
const user = await payload.create({
await payload.create({
collection: 'users',
data: {
email: devUser.email,
@@ -331,8 +362,8 @@ export default buildConfigWithDefaults({
await payload.create({
collection: slug,
data: {
title: 'has custom ID relation',
relationToCustomID: 1,
title: 'has custom ID relation',
},
})
@@ -353,23 +384,23 @@ export default buildConfigWithDefaults({
await payload.create({
collection: slug,
data: {
title: 'with-description',
description: 'description',
title: 'with-description',
},
})
await payload.create({
collection: slug,
data: {
title: 'numPost1',
number: 1,
title: 'numPost1',
},
})
await payload.create({
collection: slug,
data: {
title: 'numPost2',
number: 2,
title: 'numPost2',
},
})
@@ -390,15 +421,15 @@ export default buildConfigWithDefaults({
await payload.create({
collection: slug,
data: {
title: 'rel to hasMany',
relationHasManyField: rel1.id,
title: 'rel to hasMany',
},
})
await payload.create({
collection: slug,
data: {
title: 'rel to hasMany 2',
relationHasManyField: rel2.id,
title: 'rel to hasMany 2',
},
})
@@ -406,11 +437,11 @@ export default buildConfigWithDefaults({
await payload.create({
collection: slug,
data: {
title: 'rel to multi',
relationMultiRelationTo: {
relationTo: relationSlug,
value: rel2.id,
},
title: 'rel to multi',
},
})
@@ -418,7 +449,6 @@ export default buildConfigWithDefaults({
await payload.create({
collection: slug,
data: {
title: 'rel to multi hasMany',
relationMultiRelationToHasMany: [
{
relationTo: relationSlug,
@@ -429,6 +459,7 @@ export default buildConfigWithDefaults({
value: rel2.id,
},
],
title: 'rel to multi hasMany',
},
})

View File

@@ -5,7 +5,8 @@ import type { Post } from './payload-types'
import payload from '../../packages/payload/src'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import { initPayloadTest } from '../helpers/configHelpers'
import configPromise, { pointSlug, slug } from './config'
import { idToString } from '../helpers/idToString'
import configPromise, { errorOnHookSlug, pointSlug, slug } from './config'
const title = 'title'
@@ -42,8 +43,7 @@ describe('collections-graphql', () => {
beforeEach(async () => {
existingDoc = await createPost()
existingDocGraphQLID =
payload.db.defaultIDType === 'number' ? existingDoc.id : `"${existingDoc.id}"`
existingDocGraphQLID = idToString(existingDoc.id, payload)
})
it('should create', async () => {
@@ -67,7 +67,7 @@ describe('collections-graphql', () => {
title
}
}`
const response = await client.request(query, { title })
const response = (await client.request(query, { title })) as any
const doc: Post = response.createPost
expect(doc).toMatchObject({ title })
@@ -102,6 +102,92 @@ describe('collections-graphql', () => {
expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id }))
})
it('should read using multiple queries', async () => {
const query = `query {
postIDs: Posts {
docs {
id
}
}
posts: Posts {
docs {
id
title
}
}
}`
const response = await client.request(query)
const { postIDs, posts } = response
expect(postIDs.docs).toBeDefined()
expect(posts.docs).toBeDefined()
})
it('should commit or rollback multiple mutations independently', async () => {
const firstTitle = 'first title'
const secondTitle = 'second title'
const first = await payload.create({
collection: errorOnHookSlug,
data: {
errorBeforeChange: true,
title: firstTitle,
},
})
const second = await payload.create({
collection: errorOnHookSlug,
data: {
errorBeforeChange: true,
title: secondTitle,
},
})
const updated = 'updated title'
const query = `mutation {
createPost(data: {title: "${title}"}) {
id
title
}
updateFirst: updateErrorOnHook(id: ${idToString(
first.id,
payload,
)}, data: {title: "${updated}"}) {
title
}
updateSecond: updateErrorOnHook(id: ${idToString(
second.id,
payload,
)}, data: {title: "${updated}"}) {
id
title
}
}`
client.requestConfig.errorPolicy = 'all'
const response = await client.request(query)
client.requestConfig.errorPolicy = 'none'
const createdResult = await payload.findByID({
id: response.createPost.id,
collection: slug,
})
const updateFirstResult = await payload.findByID({
id: first.id,
collection: errorOnHookSlug,
})
const updateSecondResult = await payload.findByID({
id: second.id,
collection: errorOnHookSlug,
})
expect(response?.createPost.id).toBeDefined()
expect(response?.updateFirst).toBeNull()
expect(response?.updateSecond).toBeNull()
expect(createdResult).toMatchObject(response.createPost)
expect(updateFirstResult).toMatchObject(first)
expect(updateSecondResult).toStrictEqual(second)
})
it('should retain payload api', async () => {
const query = `
query {
@@ -685,11 +771,11 @@ describe('collections-graphql', () => {
// language=graphQL
const query = `query {
Posts(where: { title: { exists: true }}) {
docs {
badFieldName
Posts(where: { title: { exists: true }}) {
docs {
badFieldName
}
}
}
}`
await client.request(query).catch((err) => {
error = err
@@ -702,12 +788,12 @@ describe('collections-graphql', () => {
let error
// language=graphQL
const query = `mutation {
createPost(data: {min: 1}) {
id
min
createdAt
updatedAt
}
createPost(data: {min: 1}) {
id
min
createdAt
updatedAt
}
}`
await client.request(query).catch((err) => {
@@ -722,21 +808,21 @@ describe('collections-graphql', () => {
let error
// language=graphQL
const query = `mutation createTest {
test1:createUser(data: { email: "test@test.com", password: "test" }) {
email
}
test1:createUser(data: { email: "test@test.com", password: "test" }) {
email
}
test2:createUser(data: { email: "test2@test.com", password: "" }) {
email
}
test2:createUser(data: { email: "test2@test.com", password: "" }) {
email
}
test3:createUser(data: { email: "test@test.com", password: "test" }) {
email
}
test3:createUser(data: { email: "test@test.com", password: "test" }) {
email
}
test4:createUser(data: { email: "", password: "test" }) {
email
}
test4:createUser(data: { email: "", password: "test" }) {
email
}
}`
await client.request(query).catch((err) => {
@@ -775,9 +861,9 @@ describe('collections-graphql', () => {
let error
// language=graphQL
const query = `query {
QueryWithInternalError {
text
}
QueryWithInternalError {
text
}
}`
await client.request(query).catch((err) => {

View File

@@ -14,6 +14,12 @@ const RelationshipFields: CollectionConfig = {
required: true,
type: 'relationship',
},
{
name: 'relationHasManyPolymorphic',
type: 'relationship',
relationTo: ['text-fields', 'array-fields'],
hasMany: true,
},
{
name: 'relationToSelf',
relationTo: relationshipFieldsSlug,

View File

@@ -0,0 +1,9 @@
'use client'
import React from 'react'
const AfterInputImpl: React.FC = () => {
return <label className="after-input">#after-input</label>
}
export const AfterInput = <AfterInputImpl />

View File

@@ -0,0 +1,9 @@
'use client'
import React from 'react'
const BeforeInputImpl: React.FC = () => {
return <label className="before-input">#before-input</label>
}
export const BeforeInput = <BeforeInputImpl />

View File

@@ -0,0 +1,15 @@
import React from 'react'
import type { Props } from '../../../../packages/payload/src/admin/components/forms/Error/types'
const CustomError: React.FC<Props> = (props) => {
const { showError = false } = props
if (showError) {
return <div className="custom-error">#custom-error</div>
}
return null
}
export default CustomError

View File

@@ -0,0 +1,13 @@
'use client'
import React from 'react'
const CustomLabel: React.FC<{ htmlFor: string }> = ({ htmlFor }) => {
return (
<label htmlFor={htmlFor} className="custom-label">
#label
</label>
)
}
export default CustomLabel

View File

@@ -1,51 +1,51 @@
import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types'
import { textFieldsSlug } from '../../slugs'
export const defaultText = 'default-text'
import { AfterInput } from './AfterInput'
import { BeforeInput } from './BeforeInput'
import CustomError from './CustomError'
import CustomLabel from './CustomLabel'
import { defaultText, textFieldsSlug } from './shared'
const TextFields: CollectionConfig = {
slug: textFieldsSlug,
admin: {
useAsTitle: 'text',
},
fields: [
{
name: 'text',
type: 'text',
required: true,
type: 'text',
},
{
name: 'localizedText',
type: 'text',
localized: true,
type: 'text',
},
{
name: 'i18nText',
type: 'text',
label: {
en: 'Text en',
es: 'Text es',
},
admin: {
placeholder: {
en: 'en placeholder',
es: 'es placeholder',
},
description: {
en: 'en description',
es: 'es description',
},
placeholder: {
en: 'en placeholder',
es: 'es placeholder',
},
},
label: {
en: 'Text en',
es: 'Text es',
},
type: 'text',
},
{
name: 'defaultFunction',
type: 'text',
defaultValue: () => defaultText,
type: 'text',
},
{
name: 'defaultAsync',
type: 'text',
defaultValue: async (): Promise<string> => {
return new Promise((resolve) =>
setTimeout(() => {
@@ -53,25 +53,25 @@ const TextFields: CollectionConfig = {
}, 1),
)
},
type: 'text',
},
{
label: 'Override the 40k text length default',
name: 'overrideLength',
type: 'text',
label: 'Override the 40k text length default',
maxLength: 50000,
type: 'text',
},
{
name: 'fieldWithDefaultValue',
type: 'text',
defaultValue: async () => {
const defaultValue = new Promise((resolve) => setTimeout(() => resolve('some-value'), 1000))
return defaultValue
},
type: 'text',
},
{
name: 'dependentOnFieldWithDefaultValue',
type: 'text',
hooks: {
beforeChange: [
({ data }) => {
@@ -79,13 +79,39 @@ const TextFields: CollectionConfig = {
},
],
},
type: 'text',
},
{
name: 'customLabel',
admin: {
components: {
Label: CustomLabel,
},
},
type: 'text',
},
{
name: 'customError',
admin: {
components: {
Error: CustomError,
},
},
minLength: 3,
type: 'text',
},
{
name: 'beforeAndAfterInput',
admin: {
components: {
AfterInput: [AfterInput],
BeforeInput: [BeforeInput],
},
},
type: 'text',
},
],
}
export const textDoc = {
text: 'Seeded text document',
localizedText: 'Localized text',
slug: textFieldsSlug,
}
export default TextFields

View File

@@ -0,0 +1,6 @@
export const defaultText = 'default-text'
export const textFieldsSlug = 'text-fields'
export const textDoc = {
text: 'Seeded text document',
localizedText: 'Localized text',
}

View File

@@ -6,13 +6,13 @@ import path from 'path'
import payload from '../../packages/payload/src'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import wait from '../../packages/payload/src/utilities/wait'
import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers'
import { exactText, saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers'
import { AdminUrlUtil } from '../helpers/adminUrlUtil'
import { initPayloadE2E } from '../helpers/configHelpers'
import { RESTClient } from '../helpers/rest'
import { jsonDoc } from './collections/JSON'
import { numberDoc } from './collections/Number'
import { textDoc } from './collections/Text'
import { textDoc } from './collections/Text/shared'
import { lexicalE2E } from './lexicalE2E'
import { clearAndSeedEverything } from './seed'
import {
@@ -23,7 +23,7 @@ import {
textFieldsSlug,
} from './slugs'
const { afterEach, beforeAll, describe, beforeEach } = test
const { afterEach, beforeAll, beforeEach, describe } = test
let client: RESTClient
let page: Page
@@ -34,7 +34,7 @@ describe('fields', () => {
beforeAll(async ({ browser }) => {
const config = await initPayloadE2E(__dirname)
serverURL = config.serverURL
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
const context = await browser.newContext()
@@ -43,13 +43,13 @@ describe('fields', () => {
beforeEach(async () => {
await clearAndSeedEverything(payload)
await client.logout()
client = new RESTClient(null, { serverURL, defaultSlug: 'users' })
client = new RESTClient(null, { defaultSlug: 'users', serverURL })
await client.login()
})
describe('text', () => {
let url: AdminUrlUtil
beforeAll(() => {
url = new AdminUrlUtil(serverURL, 'text-fields')
url = new AdminUrlUtil(serverURL, textFieldsSlug)
})
test('should display field in list view', async () => {
@@ -80,6 +80,40 @@ describe('fields', () => {
const description = page.locator('.field-description-i18nText')
await expect(description).toHaveText('en description')
})
test('should render custom label', async () => {
await page.goto(url.create)
const label = page.locator('label.custom-label[for="field-customLabel"]')
await expect(label).toHaveText('#label')
})
test('should render custom error', async () => {
await page.goto(url.create)
const input = page.locator('input[id="field-customError"]')
await input.fill('ab')
await expect(input).toHaveValue('ab')
const error = page.locator('.custom-error:near(input[id="field-customError"])')
const submit = page.locator('button[type="button"][id="action-save"]')
await submit.click()
await expect(error).toHaveText('#custom-error')
})
test('should render BeforeInput and AfterInput', async () => {
await page.goto(url.create)
const input = page.locator('input[id="field-beforeAndAfterInput"]')
const prevSibling = await input.evaluateHandle((el) => {
return el.previousElementSibling
})
const prevSiblingText = await page.evaluate((el) => el.textContent, prevSibling)
await expect(prevSiblingText).toEqual('#before-input')
const nextSibling = await input.evaluateHandle((el) => {
return el.nextElementSibling
})
const nextSiblingText = await page.evaluate((el) => el.textContent, nextSibling)
await expect(nextSiblingText).toEqual('#after-input')
})
})
describe('number', () => {
@@ -166,11 +200,11 @@ describe('fields', () => {
await payload.create({
collection: 'indexed-fields',
data: {
text: 'text',
uniqueText,
group: {
unique: uniqueText,
},
text: 'text',
uniqueText,
},
})
@@ -286,17 +320,17 @@ describe('fields', () => {
filledGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
point: [5, 5],
localized: [4, 2],
group: { point: [4, 2] },
localized: [4, 2],
point: [5, 5],
},
})
emptyGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
point: [5, 5],
localized: [3, -2],
group: {},
localized: [3, -2],
point: [5, 5],
},
})
})
@@ -1139,8 +1173,8 @@ describe('fields', () => {
describe('EST', () => {
test.use({
geolocation: {
longitude: -83.0458,
latitude: 42.3314,
longitude: -83.0458,
},
timezoneId: 'America/Detroit',
})
@@ -1160,7 +1194,7 @@ describe('fields', () => {
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
@@ -1169,8 +1203,8 @@ describe('fields', () => {
describe('PST', () => {
test.use({
geolocation: {
longitude: -122.419416,
latitude: 37.774929,
longitude: -122.419416,
},
timezoneId: 'America/Los_Angeles',
})
@@ -1191,7 +1225,7 @@ describe('fields', () => {
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
@@ -1200,8 +1234,8 @@ describe('fields', () => {
describe('ST', () => {
test.use({
geolocation: {
longitude: -171.857,
latitude: -14.5994,
longitude: -171.857,
},
timezoneId: 'Pacific/Apia',
})
@@ -1222,7 +1256,7 @@ describe('fields', () => {
const id = routeSegments.pop()
// fetch the doc (need the date string from the DB)
const { doc } = await client.findByID({ id, slug: 'date-fields', auth: true })
const { doc } = await client.findByID({ id, auth: true, slug: 'date-fields' })
expect(doc.default).toEqual('2023-02-07T12:00:00.000Z')
})
@@ -1245,7 +1279,7 @@ describe('fields', () => {
})
const relationshipIDs = allRelationshipDocs.docs.map((doc) => doc.id)
await mapAsync(relationshipIDs, async (id) => {
await payload.delete({ collection: relationshipFieldsSlug, id })
await payload.delete({ id, collection: relationshipFieldsSlug })
})
})
@@ -1354,6 +1388,41 @@ describe('fields', () => {
await expect(field.locator('.rs__placeholder')).toBeVisible()
})
test('should display `hasMany` polymorphic relationships', async () => {
await page.goto(url.create)
const field = page.locator('#field-relationHasManyPolymorphic')
await field.click()
await page
.locator('.rs__option', {
hasText: exactText('Seeded text document'),
})
.click()
await expect(
page
.locator('#field-relationHasManyPolymorphic .relationship--multi-value-label__text', {
hasText: exactText('Seeded text document'),
})
.first(),
).toBeVisible()
// await fill the required fields then save the document and check again
await page.locator('#field-relationship').click()
await page.locator('#field-relationship .rs__option:has-text("Seeded text document")').click()
await saveDocAndAssert(page)
const valueAfterSave = page.locator('#field-relationHasManyPolymorphic .multi-value').first()
await expect(
valueAfterSave
.locator('.relationship--multi-value-label__text', {
hasText: exactText('Seeded text document'),
})
.first(),
).toBeVisible()
})
test('should populate relationship dynamic default value', async () => {
await page.goto(url.create)
await expect(

View File

@@ -23,7 +23,7 @@ import {
namedTabDefaultValue,
namedTabText,
} from './collections/Tabs/constants'
import { defaultText } from './collections/Text'
import { defaultText } from './collections/Text/shared'
import { clearAndSeedEverything } from './seed'
import { arrayFieldsSlug, groupFieldsSlug, relationshipFieldsSlug, tabsFieldsSlug } from './slugs'

View File

@@ -19,7 +19,7 @@ import { radiosDoc } from './collections/Radio'
import { richTextBulletsDoc, richTextDoc } from './collections/RichText/data'
import { selectsDoc } from './collections/Select'
import { tabsDoc } from './collections/Tabs'
import { textDoc } from './collections/Text'
import { textDoc } from './collections/Text/shared'
import { uploadsDoc } from './collections/Upload'
import {
blockFieldsSlug,
@@ -45,11 +45,8 @@ import {
export async function clearAndSeedEverything(_payload: Payload) {
return await seedDB({
snapshotKey: 'fieldsTest',
shouldResetDB: true,
collectionSlugs,
_payload,
uploadsDir: path.resolve(__dirname, './collections/Upload/uploads'),
collectionSlugs,
seedFunction: async (_payload) => {
const jpgPath = path.resolve(__dirname, './collections/Upload/payload.jpg')
const pngPath = path.resolve(__dirname, './uploads/payload.png')
@@ -143,5 +140,8 @@ export async function clearAndSeedEverything(_payload: Payload) {
_payload.create({ collection: numberFieldsSlug, data: numberDoc }),
])
},
shouldResetDB: true,
snapshotKey: 'fieldsTest',
uploadsDir: path.resolve(__dirname, './collections/Upload/uploads'),
})
}

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