Compare commits

..

28 Commits

Author SHA1 Message Date
Elliot DeNolf
fbec3a33e0 chore(release): richtext-slate/1.5.2 [skip ci] 2024-05-03 11:23:16 -04:00
Elliot DeNolf
034be89bdb chore(release): richtext-lexical/0.9.3 [skip ci] 2024-05-03 11:23:00 -04:00
Elliot DeNolf
1f2af0963b chore(release): plugin-cloud/3.0.1 [skip ci] 2024-05-03 11:22:42 -04:00
Elliot DeNolf
20f1ece2d7 chore(release): payload/2.15.0 [skip ci] 2024-05-03 11:21:26 -04:00
Jessica Chowdhury
2be5ad0eba fix: hide unusable fields from collection filter select (#6135) 2024-05-03 11:01:47 -04:00
Kendell Joseph
5c58bd322d feat: add isSortable to arrays and blocks (#5962) 2024-05-03 10:38:02 -04:00
Dan Ribbens
23f3eb1cf0 feat: use filterOptions in list relationship filter (#6156) 2024-05-03 10:26:49 -04:00
Patrik
af67749e49 fix: incorrect localesNotSaved translation (#5996) 2024-05-03 10:22:06 -04:00
Patrik
43dab5c705 fix: sanitizes fields in default edit view for drawer content (#6175) 2024-05-03 10:16:24 -04:00
Patrik
9b7e62dc20 fix: properly adds readonly styles to disabled radio fields (#6176) 2024-05-03 10:07:52 -04:00
Patrik
6e38cc2bcf fix: resets filter state when param state change within route (#6169) 2024-05-02 13:21:51 -04:00
Tylan Davis
83551bfcaa docs: adjust line breaks in code blocks - v2 (#6002) 2024-05-01 15:58:15 -04:00
Francisco Lourenço
7b44d9d28a docs: fix outdated #aliasing-server-only-modules urls (#5014) 2024-05-01 15:52:18 -04:00
ovalice
182d5db6de docs: Fix RowLabel code snippet that causes compilation error (#4947)
Co-authored-by: smarten <user@example.com>
2024-05-01 15:42:43 -04:00
Mina Sameh
93109ec84a docs: edit code of generate email subject in verify auth section (#4607) 2024-05-01 15:31:24 -04:00
Take Weiland
4d9e0f35f0 docs: clarify docs around direction transaction access (#3648) 2024-05-01 15:22:48 -04:00
Carlo Taleon
19327c8d6d docs: slate linebreak serialization in 'Generating HTML' example (#3804) 2024-05-01 15:21:03 -04:00
Elliot DeNolf
831f1ff5be fix(plugin-cloud): purge cache for all sizes (#5301) 2024-05-01 15:15:16 -04:00
Jarrod Flesch
a8ac8b4633 fix: cascade draft arg in nested GraphQL relationship queries (#6141) 2024-04-30 14:19:20 -04:00
Alessio Gravili
36b1f5a763 fix(richtext-lexical): floating toolbar caret positioned incorrectly for some line heights (#6151) 2024-04-30 12:06:02 -04:00
Alessio Gravili
24f697219b fix(richtext-lexical): drag and add block handles disappear too quickly for smaller screen sizes. (#6145) 2024-04-30 10:52:37 -04:00
Jarrod Flesch
3fccd34abe fix: GraphQL nested relationships not respecting req locale (#6117) 2024-04-29 16:32:33 -04:00
Friggo
a38f8e93a6 chore: Czech translation improvement (#6079) 2024-04-28 07:57:47 -04:00
Dan Ribbens
84570e6e3b fix: bulk publish from collection list (#6063) 2024-04-28 07:30:37 -04:00
Friggo
5ad8e0edcb chore: Czech translation improvements (#6077) 2024-04-28 07:27:26 -04:00
Jarrod Flesch
91bac9c0aa fix: version restoration (#6039) 2024-04-26 16:15:14 -04:00
Elliot DeNolf
33f6edc9d5 chore(release): richtext-slate/1.5.1 [skip ci] 2024-04-26 16:05:01 -04:00
Elliot DeNolf
e1f91f5170 chore(release): richtext-lexical/0.9.2 [skip ci] 2024-04-26 16:04:37 -04:00
102 changed files with 1814 additions and 323 deletions

7
.vscode/launch.json vendored
View File

@@ -19,6 +19,13 @@
"PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER": "s3"
}
},
{
"command": "pnpm run dev collections-graphql",
"cwd": "${workspaceFolder}",
"name": "Run Dev GraphQL",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev fields",
"cwd": "${workspaceFolder}",

View File

@@ -1,3 +1,27 @@
## [2.15.0](https://github.com/payloadcms/payload/compare/v2.14.2...v2.15.0) (2024-05-03)
### Features
* add isSortable to arrays and blocks ([#5962](https://github.com/payloadcms/payload/issues/5962)) ([5c58bd3](https://github.com/payloadcms/payload/commit/5c58bd322da966fe610959df13dfd49add35a2ef))
* use filterOptions in list relationship filter ([#6156](https://github.com/payloadcms/payload/issues/6156)) ([23f3eb1](https://github.com/payloadcms/payload/commit/23f3eb1cf0b75a4044319d7cd3e5000d5b4e42c4))
### Bug Fixes
* bulk publish from collection list ([#6063](https://github.com/payloadcms/payload/issues/6063)) ([84570e6](https://github.com/payloadcms/payload/commit/84570e6e3bbb81fcae80da92b01bc56d09906072))
* cascade draft arg in nested GraphQL relationship queries ([#6141](https://github.com/payloadcms/payload/issues/6141)) ([a8ac8b4](https://github.com/payloadcms/payload/commit/a8ac8b463349664f3188ae77217f037da72f796b))
* GraphQL nested relationships not respecting req locale ([#6117](https://github.com/payloadcms/payload/issues/6117)) ([3fccd34](https://github.com/payloadcms/payload/commit/3fccd34abe5a332f88f5e950b755cd1d21441fb6))
* hide unusable fields from collection filter select ([#6135](https://github.com/payloadcms/payload/issues/6135)) ([2be5ad0](https://github.com/payloadcms/payload/commit/2be5ad0ebafd1d3c1c0567e2085ccfd593f18271))
* incorrect `localesNotSaved` translation ([#5996](https://github.com/payloadcms/payload/issues/5996)) ([af67749](https://github.com/payloadcms/payload/commit/af67749e49db92e675b63b52190e562468894706))
* **plugin-cloud:** purge cache for all sizes ([#5301](https://github.com/payloadcms/payload/issues/5301)) ([831f1ff](https://github.com/payloadcms/payload/commit/831f1ff5bed7e083cc076e9eb5ff9a2b2f1ed710))
* properly adds `readonly` styles to disabled `radio` fields ([#6176](https://github.com/payloadcms/payload/issues/6176)) ([9b7e62d](https://github.com/payloadcms/payload/commit/9b7e62dc20dca7402c6c68dfb8a5995c211993af))
* resets filter state when param state change within route ([#6169](https://github.com/payloadcms/payload/issues/6169)) ([6e38cc2](https://github.com/payloadcms/payload/commit/6e38cc2bcfb08b608abcb6aac4b4c1f6eea63428))
* **richtext-lexical:** drag and add block handles disappear too quickly for smaller screen sizes. ([#6145](https://github.com/payloadcms/payload/issues/6145)) ([24f6972](https://github.com/payloadcms/payload/commit/24f697219b5071d91a5c37aafb50e2d823b68d4c))
* **richtext-lexical:** floating toolbar caret positioned incorrectly for some line heights ([#6151](https://github.com/payloadcms/payload/issues/6151)) ([36b1f5a](https://github.com/payloadcms/payload/commit/36b1f5a763f782c140e62aa062b4077d6efd0738))
* sanitizes fields in default edit view for drawer content ([#6175](https://github.com/payloadcms/payload/issues/6175)) ([43dab5c](https://github.com/payloadcms/payload/commit/43dab5c7053831a0c71f3a6860113f653cab674f))
* version restoration ([#6039](https://github.com/payloadcms/payload/issues/6039)) ([91bac9c](https://github.com/payloadcms/payload/commit/91bac9c0aa1ff3da052b9c2ad83fa5ac23a16d1d))
## [2.14.2](https://github.com/payloadcms/payload/compare/v2.14.1...v2.14.2) (2024-04-26)

View File

@@ -216,7 +216,7 @@ Example:
{
slug: 'customers',
auth: {
forgotPassword: {
verify: {
// highlight-start
generateEmailSubject: ({ req, user }) => {
return `Hey ${user.email}, reset your password!`;

View File

@@ -49,7 +49,8 @@ export default buildConfig({
{
label: 'Arabic',
code: 'ar',
// opt-in to setting default text-alignment on Input fields to rtl (right-to-left) when current locale is rtl
// opt-in to setting default text-alignment on Input fields to rtl (right-to-left)
// when current locale is rtl
rtl: true,
},
],
@@ -134,13 +135,9 @@ to support localization, you need to specify each field that you would like to l
```js
{
name: 'title',
type
:
'text',
// highlight-start
localized
:
true,
type: 'text',
// highlight-start
localized: true,
// highlight-end
}
```

View File

@@ -20,7 +20,8 @@ The initial request made to Payload will begin a new transaction and attach it t
```ts
const afterChange: CollectionAfterChangeHook = async ({ req }) => {
// because req.transactionID is assigned from Payload and passed through, my-slug will only persist if the entire request is successful
// because req.transactionID is assigned from Payload and passed through,
// my-slug will only persist if the entire request is successful
await req.payload.create({
req,
collection: 'my-slug',
@@ -60,10 +61,44 @@ const afterChange: CollectionAfterChangeHook = async ({ req }) => {
### Direct Transaction Access
When writing your own scripts or custom endpoints, you may wish to have direct control over transactions. This is useful for interacting with your database outside of Payload's local API.
When writing your own scripts or custom endpoints, you may wish to have direct control over transactions. This is useful for interacting with your database in something like a background job, outside the normal request-response flow.
The following functions can be used for managing transactions:
`payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls.
`payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes.
`payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes.
`payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls. Note that if your database does not support transactions, this will return `null`.\
`payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes.\
`payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes.
You can then use the transaction ID with Payload's local API by passing it inside the `PayloadRequest` object.
Here is an example for a "background job" function, which utilizes the direct transaction API to make sure it either succeeds completely or gets rolled back in case of an error.
```ts
async function allOrNothingJob() {
const req = {} as PayloadRequest;
req.transactionID = await payload.db.beginTransaction();
try {
await payload.create({
req, // use our manual transaction
collection: 'my-slug',
data: {
some: 'data'
}
});
await payload.create({
req, // use our manual transaction
collection: 'something-else',
data: {
some: 'data'
}
});
console.log('Everything done.');
if (req.transactionID) await payload.db.commitTransaction(req.transactionID);
} catch (e) {
console.error('Oh no, something went wrong!');
if (req.transactionID) await payload.db.rollbackTransaction(req.transactionID);
}
}
```

View File

@@ -59,6 +59,7 @@ properties:
| Option | Description |
|---------------------------|----------------------------------------------------------------------------------------------------------------------|
| **`initCollapsed`** | Set the initial collapsed state |
| **`isSortable`** | Disable order sorting by setting this value to `false` |
| **`components.RowLabel`** | Function or React component to be rendered as the label on the array row. Receives `({ data, index, path })` as args |
### Example
@@ -67,6 +68,7 @@ properties:
```ts
import { CollectionConfig } from 'payload/types'
import { RowLabelArgs } from 'payload/dist/admin/components/forms/RowLabel/types'
export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
@@ -101,7 +103,7 @@ export const ExampleCollection: CollectionConfig = {
],
admin: {
components: {
RowLabel: ({ data, index }) => {
RowLabel: ({ data, index }: RowLabelArgs) => {
return data?.title || `Slide ${String(index).padStart(2, '0')}`
},
},

View File

@@ -58,6 +58,7 @@ properties:
| Option | Description |
|---------------------|---------------------------------|
| **`initCollapsed`** | Set the initial collapsed state |
| **`isSortable`** | Disable order sorting by setting this value to `false` |
### Block configs

View File

@@ -36,7 +36,7 @@ If your Hook simply performs a side-effect, such as updating a CRM, it might be
#### Server-only execution
Payload Hooks are only triggered on the server. You can safely [remove your hooks](/docs/admin/webpack#aliasing-server-only-modules) from your Admin panel's client-side code by customizing the Webpack config, which not only keeps your Admin bundles' filesize small but also ensures that any server-side only code does not cause problems within browser environments.
Payload Hooks are only triggered on the server. You can safely [remove your hooks](/docs/admin/excluding-server-code#aliasing-server-only-modules) from your Admin panel's client-side code by customizing the Webpack config, which not only keeps your Admin bundles' filesize small but also ensures that any server-side only code does not cause problems within browser environments.
## Hook Types

View File

@@ -247,7 +247,7 @@ In the template, we have stubbed out a basic `onInitExtension` file that you can
### Webpack
If any of your files use server only packages such as fs, stripe, nodemailer, etc, they will need to be removed from the browser bundle. To do that, you can [alias the file imports with webpack](https://payloadcms.com/docs/admin/webpack#aliasing-server-only-modules).
If any of your files use server only packages such as fs, stripe, nodemailer, etc, they will need to be removed from the browser bundle. To do that, you can [alias the file imports with webpack](https://payloadcms.com/docs/admin/excluding-server-code#aliasing-server-only-modules).
When files are bundled for the browser, the import paths are essentially crawled to determine what files to include in the bundle. To prevent the server only files from making it into the bundle, we can alias their import paths to a file that can be included in the browser. This will short-circuit the import path crawling and ensure browser only code is bundled.

View File

@@ -203,7 +203,8 @@ const Pages: CollectionConfig = {
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
// The HTMLConverter Feature is the feature which manages the HTML serializers. If you do not pass any arguments to it, it will use the default serializers.
// The HTMLConverter Feature is the feature which manages the HTML serializers.
// If you do not pass any arguments to it, it will use the default serializers.
HTMLConverterFeature({}),
],
}),

View File

@@ -270,6 +270,10 @@ const serialize = (children) =>
text = <em key={i}>{text}</em>;
}
if (node.text === '') {
text = <br />;
}
// Handle other leaf types here...
return <Fragment key={i}>{text}</Fragment>;

View File

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

View File

@@ -19,6 +19,7 @@ export const ArrayAction: React.FC<Props> = ({
duplicateRow,
hasMaxRows,
index,
isSortable,
moveRow,
removeRow,
rowCount,
@@ -33,7 +34,7 @@ export const ArrayAction: React.FC<Props> = ({
render={({ close }) => {
return (
<PopupList.ButtonGroup buttonSize="small">
{index !== 0 && (
{isSortable && index !== 0 && (
<PopupList.Button
className={`${baseClass}__action ${baseClass}__move-up`}
onClick={() => {
@@ -47,7 +48,7 @@ export const ArrayAction: React.FC<Props> = ({
{t('moveUp')}
</PopupList.Button>
)}
{index < rowCount - 1 && (
{isSortable && index < rowCount - 1 && (
<PopupList.Button
className={`${baseClass}__action`}
onClick={() => {

View File

@@ -3,6 +3,7 @@ export type Props = {
duplicateRow: (current: number) => void
hasMaxRows: boolean
index: number
isSortable: boolean
moveRow: (from: number, to: number) => void
removeRow: (index: number) => void
rowCount: number

View File

@@ -139,7 +139,7 @@ const Duplicate: React.FC<Props> = ({ id, slug, collection }) => {
if (localeErrors.length > 0) {
toast.error(
`
${t('error:localesNotSaved', { count: localeErrors.length })}
${t('error:localesNotSaved_other', { count: localeErrors.length })}
${localeErrors.join(', ')}
`,
{ autoClose: 5000 },

View File

@@ -9,7 +9,6 @@ import { fieldAffectsData } from '../../../../fields/config/types'
import { getTranslation } from '../../../../utilities/getTranslation'
import Chevron from '../../icons/Chevron'
import { useSearchParams } from '../../utilities/SearchParams'
import Button from '../Button'
import ColumnSelector from '../ColumnSelector'
import DeleteMany from '../DeleteMany'
import EditMany from '../EditMany'
@@ -50,6 +49,8 @@ export const ListControls: React.FC<Props> = (props) => {
const params = useSearchParams()
const shouldInitializeWhereOpened = validateWhereQuery(params?.where)
const hasWhereParam = React.useRef(Boolean(params?.where))
const [textFieldsToBeSearched, setFieldsToBeSearched] = useState(
getTextFieldsToBeSearched(listSearchableFields, fields),
)
@@ -65,6 +66,15 @@ export const ListControls: React.FC<Props> = (props) => {
setFieldsToBeSearched(getTextFieldsToBeSearched(listSearchableFields, fields))
}, [listSearchableFields, fields])
React.useEffect(() => {
if (hasWhereParam.current && !params?.where) {
setVisibleDrawer(undefined)
hasWhereParam.current = false
} else if (params?.where) {
hasWhereParam.current = true
}
}, [setVisibleDrawer, params?.where])
return (
<div className={baseClass}>
<div className={`${baseClass}__wrap`}>
@@ -147,6 +157,7 @@ export const ListControls: React.FC<Props> = (props) => {
<WhereBuilder
collection={collection}
handleChange={handleWhereChange}
key={String(hasWhereParam.current && !params?.where)}
modifySearchQuery={modifySearchQuery}
/>
</AnimateHeight>

View File

@@ -18,7 +18,7 @@ import './index.scss'
const baseClass = 'publish-many'
const PublishMany: React.FC<Props> = (props) => {
const { collection: { labels: { plural }, slug, versions } = {}, resetParams } = props
const { collection: { slug, labels: { plural }, versions } = {}, resetParams } = props
const {
routes: { api },
@@ -27,7 +27,7 @@ const PublishMany: React.FC<Props> = (props) => {
const { permissions } = useAuth()
const { toggleModal } = useModal()
const { i18n, t } = useTranslation('version')
const { count, getQueryParams, selectAll } = useSelection()
const { getQueryParams, selectAll } = useSelection()
const [submitted, setSubmitted] = useState(false)
const collectionPermissions = permissions?.collections?.[slug]
@@ -41,9 +41,11 @@ const PublishMany: React.FC<Props> = (props) => {
const handlePublish = useCallback(() => {
setSubmitted(true)
requests
void requests
.patch(
`${serverURL}${api}/${slug}${getQueryParams({ _status: { not_equals: 'published' } })}`,
`${serverURL}${api}/${slug}${getQueryParams({
_status: { not_equals: 'published' },
})}&draft=true`,
{
body: JSON.stringify({
_status: 'published',

View File

@@ -1,3 +1,6 @@
import type { Where } from 'payload/types'
import qs from 'qs'
import React, { useCallback, useEffect, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -6,6 +9,7 @@ import type { Option } from '../../../ReactSelect/types'
import type { GetResults, Props, ValueWithRelation } from './types'
import useDebounce from '../../../../../hooks/useDebounce'
import { useAuth } from '../../../../utilities/Auth'
import { useConfig } from '../../../../utilities/Config'
import ReactSelect from '../../../ReactSelect'
import './index.scss'
@@ -16,7 +20,15 @@ const baseClass = 'condition-value-relationship'
const maxResultsPerRequest = 10
const RelationshipField: React.FC<Props> = (props) => {
const { admin: { isSortable } = {}, disabled, hasMany, onChange, relationTo, value } = props
const {
admin: { isSortable } = {},
disabled,
filterOptions,
hasMany,
onChange,
relationTo,
value,
} = props
const {
collections,
@@ -33,11 +45,12 @@ const RelationshipField: React.FC<Props> = (props) => {
const [hasLoadedFirstOptions, setHasLoadedFirstOptions] = useState(false)
const debouncedSearch = useDebounce(search, 300)
const { i18n, t } = useTranslation('general')
const { user } = useAuth()
const addOptions = useCallback(
(data, relation) => {
const collection = collections.find((coll) => coll.slug === relation)
dispatchOptions({ collection, data, hasMultipleRelations, i18n, relation, type: 'ADD' })
dispatchOptions({ type: 'ADD', collection, data, hasMultipleRelations, i18n, relation })
},
[collections, hasMultipleRelations, i18n],
)
@@ -61,23 +74,66 @@ const RelationshipField: React.FC<Props> = (props) => {
let resultsFetched = 0
if (!errorLoading) {
relationsToFetch.reduce(async (priorRelation, relation) => {
void relationsToFetch.reduce(async (priorRelation, relation) => {
await priorRelation
if (resultsFetched < 10) {
const search: Record<string, unknown> & { where: Where } = {
depth: 0,
limit: maxResultsPerRequest,
page: lastLoadedPageToUse,
where: { and: [] },
}
const collection = collections.find((coll) => coll.slug === relation)
const fieldToSearch = collection?.admin?.useAsTitle || 'id'
const searchParam = searchArg ? `&where[${fieldToSearch}][like]=${searchArg}` : ''
const response = await fetch(
`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`,
{
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
// add search arg to where object
if (searchArg) {
search.where.and.push({
[fieldToSearch]: {
like: searchArg,
},
})
}
// call the filterOptions function if it exists passing in the collection
if (filterOptions) {
const optionFilter =
typeof filterOptions === 'function'
? await filterOptions({
// data and siblingData are empty since we cannot fetch with the values covering the
// entire list this limitation means that filterOptions functions using a document's
// data are unsupported in the whereBuilder
id: undefined,
data: {},
relationTo: collection.slug,
siblingData: {},
user,
})
: filterOptions
if (typeof optionFilter === 'object') {
search.where.and.push(optionFilter)
}
if (optionFilter === false) {
// no options will be returned
setLastFullyLoadedRelation(relations.indexOf(relation))
// If there are more relations to search, need to reset lastLoadedPage to 1
// both locally within function and state
if (relations.indexOf(relation) + 1 < relations.length) {
lastLoadedPageToUse = 1
}
return
}
}
if (search.where.and.length === 0) {
delete search.where
}
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(search)}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
)
})
if (response.ok) {
const data: PaginatedDocs = await response.json()
@@ -103,7 +159,18 @@ const RelationshipField: React.FC<Props> = (props) => {
}, Promise.resolve())
}
},
[i18n, relationTo, errorLoading, collections, serverURL, api, addOptions, t],
[
relationTo,
errorLoading,
collections,
filterOptions,
serverURL,
api,
i18n.language,
user,
addOptions,
t,
],
)
const findOptionsByValue = useCallback((): Option | Option[] => {

View File

@@ -52,9 +52,9 @@ const Condition: React.FC<Props> = (props) => {
useEffect(() => {
dispatch({
type: 'update',
andIndex,
orIndex,
type: 'update',
value: debouncedValue || '',
})
}, [debouncedValue, dispatch, orIndex, andIndex])
@@ -80,10 +80,10 @@ const Condition: React.FC<Props> = (props) => {
isClearable={false}
onChange={(field) => {
dispatch({
andIndex: andIndex,
field: field?.value,
orIndex: orIndex,
type: 'update',
andIndex,
field: field?.value,
orIndex,
})
}}
options={fields}
@@ -96,10 +96,10 @@ const Condition: React.FC<Props> = (props) => {
isClearable={false}
onChange={(operator) => {
dispatch({
type: 'update',
andIndex,
operator: operator.value,
orIndex,
type: 'update',
})
setInternalOperatorField(operator.value)
}}
@@ -134,9 +134,9 @@ const Condition: React.FC<Props> = (props) => {
iconStyle="with-border"
onClick={() =>
dispatch({
type: 'remove',
andIndex,
orIndex,
type: 'remove',
})
}
round
@@ -148,11 +148,11 @@ const Condition: React.FC<Props> = (props) => {
iconStyle="with-border"
onClick={() =>
dispatch({
type: 'add',
andIndex: andIndex + 1,
field: fields[0].value,
orIndex,
relation: 'and',
type: 'add',
})
}
round

View File

@@ -32,34 +32,34 @@ const reduceFields = (fields, i18n) =>
} else {
operators = fieldTypes[field.type].operators
}
}
const operatorKeys = new Set()
const filteredOperators = operators.reduce((acc, operator) => {
if (!operatorKeys.has(operator.value)) {
operatorKeys.add(operator.value)
return [
...acc,
{
...operator,
label: i18n.t(`operators:${operator.label}`),
},
]
const operatorKeys = new Set()
const filteredOperators = operators.reduce((acc, operator) => {
if (!operatorKeys.has(operator.value)) {
operatorKeys.add(operator.value)
return [
...acc,
{
...operator,
label: i18n.t(`operators:${operator.label}`),
},
]
}
return acc
}, [])
const formattedField = {
label: getTranslation(field.label || field.name, i18n),
value: field.name,
...fieldTypes[field.type],
operators: filteredOperators,
props: {
...field,
},
}
return acc
}, [])
const formattedField = {
label: getTranslation(field.label || field.name, i18n),
value: field.name,
...fieldTypes[field.type],
operators: filteredOperators,
props: {
...field,
},
return [...reduced, formattedField]
}
return [...reduced, formattedField]
return reduced
}, [])
/**

View File

@@ -26,6 +26,7 @@ type ArrayRowProps = UseDraggableSortableReturn &
duplicateRow: (rowIndex: number) => void
forceRender?: boolean
hasMaxRows?: boolean
isSortable: boolean
moveRow: (fromIndex: number, toIndex: number) => void
readOnly?: boolean
removeRow: (rowIndex: number) => void
@@ -44,6 +45,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
forceRender = false,
hasMaxRows,
indexPath,
isSortable,
labels,
listeners,
moveRow,
@@ -94,6 +96,7 @@ export const ArrayRow: React.FC<ArrayRowProps> = ({
duplicateRow={duplicateRow}
hasMaxRows={hasMaxRows}
index={rowIndex}
isSortable={isSortable}
moveRow={moveRow}
removeRow={removeRow}
rowCount={rowCount}

View File

@@ -29,7 +29,7 @@ const baseClass = 'array-field'
const ArrayFieldType: React.FC<Props> = (props) => {
const {
name,
admin: { className, components, condition, description, readOnly },
admin: { className, components, condition, description, isSortable = true, readOnly },
fieldTypes,
fields,
forceRender = false,
@@ -113,7 +113,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const duplicateRow = useCallback(
(rowIndex: number) => {
dispatchFields({ path, rowIndex, type: 'DUPLICATE_ROW' })
dispatchFields({ type: 'DUPLICATE_ROW', path, rowIndex })
setModified(true)
setTimeout(() => {
@@ -133,7 +133,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const moveRow = useCallback(
(moveFromIndex: number, moveToIndex: number) => {
dispatchFields({ moveFromIndex, moveToIndex, path, type: 'MOVE_ROW' })
dispatchFields({ type: 'MOVE_ROW', moveFromIndex, moveToIndex, path })
setModified(true)
},
[dispatchFields, path, setModified],
@@ -141,14 +141,14 @@ const ArrayFieldType: React.FC<Props> = (props) => {
const toggleCollapseAll = useCallback(
(collapsed: boolean) => {
dispatchFields({ collapsed, path, setDocFieldPreferences, type: 'SET_ALL_ROWS_COLLAPSED' })
dispatchFields({ type: 'SET_ALL_ROWS_COLLAPSED', collapsed, path, setDocFieldPreferences })
},
[dispatchFields, path, setDocFieldPreferences],
)
const setCollapse = useCallback(
(rowID: string, collapsed: boolean) => {
dispatchFields({ collapsed, path, rowID, setDocFieldPreferences, type: 'SET_ROW_COLLAPSED' })
dispatchFields({ type: 'SET_ROW_COLLAPSED', collapsed, path, rowID, setDocFieldPreferences })
},
[dispatchFields, path, setDocFieldPreferences],
)
@@ -227,7 +227,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
onDragEnd={({ moveFromIndex, moveToIndex }) => moveRow(moveFromIndex, moveToIndex)}
>
{rows.map((row, i) => (
<DraggableSortableItem disabled={readOnly} id={row.id} key={row.id}>
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
{(draggableSortableItemProps) => (
<ArrayRow
{...draggableSortableItemProps}
@@ -239,6 +239,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
forceRender={forceRender}
hasMaxRows={hasMaxRows}
indexPath={indexPath}
isSortable={isSortable}
labels={labels}
moveRow={moveRow}
path={path}

View File

@@ -26,6 +26,7 @@ type BlockFieldProps = UseDraggableSortableReturn &
duplicateRow: (rowIndex: number) => void
forceRender?: boolean
hasMaxRows?: boolean
isSortable?: boolean
moveRow: (fromIndex: number, toIndex: number) => void
readOnly: boolean
removeRow: (rowIndex: number) => void
@@ -44,6 +45,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
forceRender,
hasMaxRows,
indexPath,
isSortable,
labels,
listeners,
moveRow,
@@ -90,6 +92,7 @@ export const BlockRow: React.FC<BlockFieldProps> = ({
blocks={blocks}
duplicateRow={duplicateRow}
hasMaxRows={hasMaxRows}
isSortable={isSortable}
labels={labels}
moveRow={moveRow}
removeRow={removeRow}

View File

@@ -13,6 +13,7 @@ export const RowActions: React.FC<{
blocks: Block[]
duplicateRow: (rowIndex: number, blockType: string) => void
hasMaxRows?: boolean
isSortable?: boolean
labels: Labels
moveRow: (fromIndex: number, toIndex: number) => void
removeRow: (rowIndex: number) => void
@@ -25,6 +26,7 @@ export const RowActions: React.FC<{
blocks,
duplicateRow,
hasMaxRows,
isSortable,
labels,
moveRow,
removeRow,
@@ -59,6 +61,7 @@ export const RowActions: React.FC<{
duplicateRow={() => duplicateRow(rowIndex, blockType)}
hasMaxRows={hasMaxRows}
index={rowIndex}
isSortable={isSortable}
moveRow={moveRow}
removeRow={removeRow}
rowCount={rowCount}

View File

@@ -34,7 +34,7 @@ const BlocksField: React.FC<Props> = (props) => {
const {
name,
admin: { className, condition, description, readOnly },
admin: { className, condition, description, isSortable = true, readOnly },
blocks,
fieldTypes,
forceRender = false,
@@ -230,7 +230,7 @@ const BlocksField: React.FC<Props> = (props) => {
if (blockToRender) {
return (
<DraggableSortableItem disabled={readOnly} id={row.id} key={row.id}>
<DraggableSortableItem disabled={readOnly || !isSortable} id={row.id} key={row.id}>
{(draggableSortableItemProps) => (
<BlockRow
{...draggableSortableItemProps}
@@ -242,6 +242,7 @@ const BlocksField: React.FC<Props> = (props) => {
forceRender={forceRender}
hasMaxRows={hasMaxRows}
indexPath={indexPath}
isSortable={isSortable}
labels={labels}
moveRow={moveRow}
path={path}

View File

@@ -98,6 +98,7 @@ const RadioGroupInput: React.FC<RadioGroupInputProps> = (props) => {
onChange={readOnly ? undefined : onChange}
option={optionIsObject(option) ? option : { label: option, value: option }}
path={path}
readOnly={readOnly}
/>
</li>
)

View File

@@ -70,31 +70,3 @@
}
}
}
.radio-group--read-only {
.radio-input {
cursor: default;
&__label {
color: var(--theme-elevation-800);
}
&--is-selected {
.radio-input__styled-radio {
&:before {
background-color: var(--theme-elevation-800);
}
}
}
&:not(.radio-input--is-selected) {
&:hover {
.radio-input__styled-radio {
&:before {
opacity: 0;
}
}
}
}
}
}

View File

@@ -9,7 +9,7 @@ import './index.scss'
const baseClass = 'radio-input'
const RadioInput: React.FC<Props> = (props) => {
const { isSelected, onChange, option, path } = props
const { isSelected, onChange, option, path, readOnly } = props
const { i18n } = useTranslation()
const classes = [baseClass, isSelected && `${baseClass}--is-selected`].filter(Boolean).join(' ')
@@ -21,6 +21,7 @@ const RadioInput: React.FC<Props> = (props) => {
<div className={classes}>
<input
checked={isSelected}
disabled={readOnly}
id={id}
onChange={() => (typeof onChange === 'function' ? onChange(option.value) : null)}
type="radio"

View File

@@ -8,4 +8,5 @@ export type Props = {
value: string
}
path: string
readOnly?: boolean
}

View File

@@ -29,6 +29,34 @@
}
}
.radio-group--read-only {
.radio-input {
cursor: default;
&__label {
color: var(--theme-elevation-400);
}
&--is-selected {
.radio-input__styled-radio {
&:before {
background-color: var(--theme-elevation-100);
}
}
}
&:not(.radio-input--is-selected) {
&:hover {
.radio-input__styled-radio {
&:before {
opacity: 0;
}
}
}
}
}
}
html[data-theme='light'] {
.radio-group {
&.error {

View File

@@ -55,6 +55,7 @@ const RadioGroup: React.FC<Props> = (props) => {
onChange={readOnly ? undefined : setValue}
options={options}
path={path}
readOnly={readOnly}
required={required}
showError={showError}
style={style}

View File

@@ -51,6 +51,7 @@ type RichTextAdapterBase<
context: RequestContext
currentDepth?: number
depth: number
draft: boolean
field: RichTextField<Value, AdapterProps, ExtraFieldProperties>
findMany: boolean
flattenLocales: boolean

View File

@@ -66,7 +66,9 @@ const Restore: React.FC<Props> = ({
if (res.status === 200) {
const json = await res.json()
toast.success(json.message)
history.push(redirectURL)
history.push(redirectURL, {
refetchDocumentData: true,
})
} else {
toast.error(t('problemRestoringVersion'))
}

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react'
import React, { Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { FieldTypes } from '../../../../forms/field-types'
@@ -12,6 +12,7 @@ import Meta from '../../../../utilities/Meta'
import Auth from '../Auth'
import { SetStepNav } from '../SetStepNav'
import { Upload } from '../Upload'
import formatFields from '../formatFields'
import './index.scss'
const baseClass = 'collection-default-edit'
@@ -37,7 +38,9 @@ export const DefaultCollectionEdit: React.FC<
permissions,
} = props
const { auth, fields, upload } = collection
const { auth, upload } = collection
const [fields] = useState(() => formatFields(collection, isEditing))
const operation = isEditing ? 'update' : 'create'

View File

@@ -50,7 +50,8 @@ const EditView: React.FC<IndexProps> = (props) => {
} = config
const { params: { id } = {} } = useRouteMatch<Record<string, string>>()
const history = useHistory()
const history = useHistory<{ refetchDocumentData?: boolean }>()
const [internalState, setInternalState] = useState<Fields>()
const [updatedAt, setUpdatedAt] = useState<string>()
const { permissions, user } = useAuth()
@@ -58,7 +59,7 @@ const EditView: React.FC<IndexProps> = (props) => {
const { docPermissions, getDocPermissions, getDocPreferences, getVersions } = useDocumentInfo()
const { t } = useTranslation('general')
const [{ data, isError, isLoading: isLoadingData }] = usePayloadAPI(
const [{ data, isError, isLoading: isLoadingData }, { refetchData }] = usePayloadAPI(
isEditing ? `${serverURL}${api}/${collectionSlug}/${id}` : '',
{ initialData: null, initialParams: { depth: 0, draft: 'true', 'fallback-locale': 'null' } },
)
@@ -128,10 +129,16 @@ const EditView: React.FC<IndexProps> = (props) => {
useEffect(() => {
setFormQueryParams((params) => ({
...params,
locale: locale,
locale,
}))
}, [locale])
useEffect(() => {
if (history.location.state?.refetchDocumentData) {
void refetchData()
}
}, [history.location.state?.refetchDocumentData, refetchData])
if (isError) {
return <NotFound marginTop="large" />
}

View File

@@ -1,5 +1,5 @@
import queryString from 'qs'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { requests } from '../api'
@@ -12,6 +12,7 @@ type Result = [
isLoading: boolean
},
{
refetchData: (abortController?: AbortController) => Promise<void>
setParams: React.Dispatch<unknown>
},
]
@@ -43,49 +44,53 @@ const usePayloadAPI: UsePayloadAPI = (url, options = {}) => {
},
)
const fetchData = useCallback(
async (abortController?: AbortController) => {
if (url) {
setIsError(false)
setIsLoading(true)
try {
const response = await requests.get(`${url}${search}`, {
headers: {
'Accept-Language': i18n.language,
},
signal: abortController ? abortController.signal : undefined,
})
if (response.status > 201) {
setIsError(true)
}
const json = await response.json()
setData(json)
setIsLoading(false)
} catch (error) {
if (!abortController || !abortController.signal.aborted) {
setIsError(true)
setIsLoading(false)
}
}
} else {
setIsError(false)
setIsLoading(false)
}
},
[url, search, i18n.language],
)
useEffect(() => {
const abortController = new AbortController()
const fetchData = async () => {
setIsError(false)
setIsLoading(true)
try {
const response = await requests.get(`${url}${search}`, {
headers: {
'Accept-Language': i18n.language,
},
signal: abortController.signal,
})
if (response.status > 201) {
setIsError(true)
}
const json = await response.json()
setData(json)
setIsLoading(false)
} catch (error) {
if (!abortController.signal.aborted) {
setIsError(true)
setIsLoading(false)
}
}
}
if (url) {
fetchData()
} else {
setIsError(false)
setIsLoading(false)
}
void fetchData(abortController)
return () => {
abortController.abort()
}
}, [url, locale, search, i18n.language])
}, [url, search, fetchData])
return [{ data, isError, isLoading }, { setParams }]
return [
{ data, isError, isLoading },
{ refetchData: fetchData, setParams },
]
}
export default usePayloadAPI

View File

@@ -205,6 +205,7 @@ async function login<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: user,
draft: undefined,
fallbackLocale,
global: null,
locale,

View File

@@ -54,6 +54,7 @@ const batchAndLoadDocs =
fallbackLocale,
overrideAccess,
showHiddenFields,
draft,
] = JSON.parse(key)
const batchKeyArray = [
@@ -65,6 +66,7 @@ const batchAndLoadDocs =
fallbackLocale,
overrideAccess,
showHiddenFields,
draft,
]
const batchKey = JSON.stringify(batchKeyArray)
@@ -100,6 +102,7 @@ const batchAndLoadDocs =
fallbackLocale,
overrideAccess,
showHiddenFields,
draft,
] = JSON.parse(batchKey)
req.transactionID = transactionID
@@ -109,6 +112,7 @@ const batchAndLoadDocs =
currentDepth,
depth,
disableErrors: true,
draft,
fallbackLocale,
locale,
overrideAccess: Boolean(overrideAccess),
@@ -136,6 +140,7 @@ const batchAndLoadDocs =
fallbackLocale,
overrideAccess,
showHiddenFields,
draft,
])
const docsIndex = keys.findIndex((key) => key === docKey)

View File

@@ -54,6 +54,8 @@ function initCollectionsGraphQL(payload: Payload): void {
if (!graphQL) return
const draftsEnabled = collection.config.versions?.drafts
let singularName
let pluralName
const fromSlug = formatNames(collection.config.slug)
@@ -150,7 +152,11 @@ function initCollectionsGraphQL(payload: Payload): void {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
draft: { type: GraphQLBoolean },
...(draftsEnabled
? {
draft: { type: GraphQLBoolean },
}
: {}),
...(payload.config.localization
? {
fallbackLocale: { type: payload.types.fallbackLocaleInputType },
@@ -164,7 +170,11 @@ function initCollectionsGraphQL(payload: Payload): void {
payload.Query.fields[pluralName] = {
type: buildPaginatedListType(pluralName, collection.graphQL.type),
args: {
draft: { type: GraphQLBoolean },
...(draftsEnabled
? {
draft: { type: GraphQLBoolean },
}
: {}),
where: { type: collection.graphQL.whereInputType },
...(payload.config.localization
? {
@@ -187,7 +197,11 @@ function initCollectionsGraphQL(payload: Payload): void {
},
}),
args: {
draft: { type: GraphQLBoolean },
...(draftsEnabled
? {
draft: { type: GraphQLBoolean },
}
: {}),
where: { type: collection.graphQL.whereInputType },
...(payload.config.localization
? {
@@ -217,7 +231,11 @@ function initCollectionsGraphQL(payload: Payload): void {
...(createMutationInputType
? { data: { type: collection.graphQL.mutationInputType } }
: {}),
draft: { type: GraphQLBoolean },
...(draftsEnabled
? {
draft: { type: GraphQLBoolean },
}
: {}),
...(payload.config.localization
? {
locale: { type: payload.types.localeInputType },
@@ -235,7 +253,11 @@ function initCollectionsGraphQL(payload: Payload): void {
...(updateMutationInputType
? { data: { type: collection.graphQL.updateMutationInputType } }
: {}),
draft: { type: GraphQLBoolean },
...(draftsEnabled
? {
draft: { type: GraphQLBoolean },
}
: {}),
...(payload.config.localization
? {
locale: { type: payload.types.localeInputType },

View File

@@ -28,6 +28,8 @@ export default function findResolver(collection: Collection): Resolver {
req.locale = args.locale || locale
req.fallbackLocale = fallbackLocale
context.req = req
const options = {
collection,
req: isolateObjectProperty<PayloadRequest>(req, 'transactionID'),

View File

@@ -33,6 +33,17 @@ export default function createResolver<TSlug extends keyof GeneratedTypes['colle
const locale = req.locale
req = isolateObjectProperty(req, 'locale')
req.locale = args.locale || locale
if (!req.query) req.query = {}
const draft: boolean =
args.draft ?? req.query?.draft === 'false'
? false
: req.query?.draft === 'true'
? true
: undefined
if (typeof draft === 'boolean') req.query.draft = String(draft)
context.req = req
const options = {
collection,

View File

@@ -31,6 +31,17 @@ export default function getDeleteResolver<TSlug extends keyof GeneratedTypes['co
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
if (!req.query) req.query = {}
const draft: boolean =
args.draft ?? req.query?.draft === 'false'
? false
: req.query?.draft === 'true'
? true
: undefined
if (typeof draft === 'boolean') req.query.draft = String(draft)
context.req = req
const options = {
id: args.id,

View File

@@ -35,6 +35,17 @@ export default function findResolver(collection: Collection): Resolver {
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
if (!req.query) req.query = {}
const draft: boolean =
args.draft ?? req.query?.draft === 'false'
? false
: req.query?.draft === 'true'
? true
: undefined
if (typeof draft === 'boolean') req.query.draft = String(draft)
context.req = req
const options = {
collection,

View File

@@ -30,6 +30,17 @@ export default function findByIDResolver<T extends keyof GeneratedTypes['collect
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
if (!req.query) req.query = {}
const draft: boolean =
args.draft ?? req.query?.draft === 'false'
? false
: req.query?.draft === 'true'
? true
: undefined
if (typeof draft === 'boolean') req.query.draft = String(draft)
context.req = req
const options = {
id: args.id,

View File

@@ -11,7 +11,6 @@ import findVersionByID from '../../operations/findVersionByID'
export type Resolver<T extends TypeWithID = any> = (
_: unknown,
args: {
draft: boolean
fallbackLocale?: string
id: number | string
locale?: string
@@ -32,11 +31,12 @@ export default function findVersionByIDResolver(collection: Collection): Resolve
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
context.req = req
const options = {
id: args.id,
collection,
depth: 0,
draft: args.draft,
req: isolateObjectProperty<PayloadRequest>(req, 'transactionID'),
}

View File

@@ -35,6 +35,17 @@ export default function findVersionsResolver(collection: Collection): Resolver {
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
if (!req.query) req.query = {}
const draft: boolean =
args.draft ?? req.query?.draft === 'false'
? false
: req.query?.draft === 'true'
? true
: undefined
if (typeof draft === 'boolean') req.query.draft = String(draft)
context.req = req
const options = {
collection,

View File

@@ -34,6 +34,17 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['colle
req = isolateObjectProperty(req, 'fallbackLocale')
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
if (!req.query) req.query = {}
const draft: boolean =
args.draft ?? req.query?.draft === 'false'
? false
: req.query?.draft === 'true'
? true
: undefined
if (typeof draft === 'boolean') req.query.draft = String(draft)
context.req = req
const options = {
id: args.id,

View File

@@ -290,6 +290,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
draft,
fallbackLocale,
global: null,
locale,

View File

@@ -179,6 +179,7 @@ async function deleteOperation<TSlug extends keyof GeneratedTypes['collections']
context: req.context,
depth,
doc: result || doc,
draft: undefined,
fallbackLocale,
global: null,
locale,

View File

@@ -158,6 +158,7 @@ async function deleteByID<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
draft: undefined,
fallbackLocale,
global: null,
locale,

View File

@@ -196,6 +196,7 @@ async function find<T extends TypeWithID & Record<string, unknown>>(
currentDepth,
depth,
doc,
draft: draftsEnabled,
fallbackLocale,
findMany: true,
global: null,

View File

@@ -139,6 +139,7 @@ async function findByID<T extends TypeWithID>(incomingArgs: Arguments): Promise<
currentDepth,
depth,
doc: result,
draft: draftEnabled,
fallbackLocale,
global: null,
locale,

View File

@@ -112,6 +112,7 @@ async function findVersionByID<T extends TypeWithID = any>(
currentDepth,
depth,
doc: result.version,
draft: undefined,
fallbackLocale,
global: null,
locale,

View File

@@ -125,6 +125,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
context: req.context,
depth,
doc: data.version,
draft: undefined,
fallbackLocale,
findMany: true,
global: null,

View File

@@ -140,6 +140,7 @@ async function restoreVersion<T extends TypeWithID = any>(args: Arguments): Prom
context: req.context,
depth,
doc: result,
draft: undefined,
fallbackLocale,
global: null,
locale,

View File

@@ -177,6 +177,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth: 0,
doc,
draft: draftArg,
fallbackLocale,
global: null,
locale,
@@ -312,6 +313,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
draft: draftArg,
fallbackLocale: null,
global: null,
locale,

View File

@@ -131,6 +131,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth: 0,
doc: docWithLocales,
draft: draftArg,
fallbackLocale: null,
global: null,
locale,
@@ -300,6 +301,7 @@ async function updateByID<TSlug extends keyof GeneratedTypes['collections']>(
context: req.context,
depth,
doc: result,
draft: draftArg,
fallbackLocale,
global: null,
locale,

View File

@@ -311,6 +311,7 @@ export const array = baseField.keys({
RowLabel: componentSchema,
})
.default({}),
isSortable: joi.boolean(),
})
.default({}),
dbName: joi.alternatives().try(joi.string(), joi.func()),
@@ -408,6 +409,11 @@ export const relationship = baseField.keys({
export const blocks = baseField.keys({
name: joi.string().required(),
type: joi.string().valid('blocks').required(),
admin: baseAdminFields
.keys({
isSortable: joi.boolean(),
})
.default({}),
blocks: joi
.array()
.items(

View File

@@ -564,6 +564,10 @@ export type ArrayField = FieldBase & {
RowLabel?: RowLabel
} & Admin['components']
initCollapsed?: boolean | false
/**
* Disable drag and drop sorting
*/
isSortable?: boolean
}
/**
* Customize the SQL table name
@@ -631,6 +635,10 @@ export type Block = {
export type BlockField = FieldBase & {
admin?: Admin & {
initCollapsed?: boolean | false
/**
* Disable drag and drop sorting
*/
isSortable?: boolean
}
blocks: Block[]
defaultValue?: unknown

View File

@@ -11,6 +11,7 @@ type Args = {
currentDepth?: number
depth: number
doc: Record<string, unknown>
draft: boolean
fallbackLocale: null | string
findMany?: boolean
flattenLocales?: boolean
@@ -28,6 +29,7 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
currentDepth: incomingCurrentDepth,
depth: incomingDepth,
doc: incomingDoc,
draft,
fallbackLocale,
findMany,
flattenLocales = true,
@@ -56,6 +58,7 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: collection?.fields || global?.fields,

View File

@@ -16,6 +16,7 @@ type Args = {
currentDepth: number
depth: number
doc: Record<string, unknown>
draft: boolean
fallbackLocale: null | string
field: Field | TabAsField
fieldPromises: Promise<void>[]
@@ -46,6 +47,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
field,
fieldPromises,
@@ -145,6 +147,7 @@ export const promise = async ({
context,
currentDepth,
depth,
draft,
field,
findMany,
flattenLocales,
@@ -287,6 +290,7 @@ export const promise = async ({
relationshipPopulationPromise({
currentDepth,
depth,
draft,
fallbackLocale,
field,
locale,
@@ -310,6 +314,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.fields,
@@ -340,6 +345,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.fields,
@@ -366,6 +372,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.fields,
@@ -404,6 +411,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: block.fields,
@@ -434,6 +442,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: block.fields,
@@ -468,6 +477,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.fields,
@@ -500,6 +510,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.fields,
@@ -526,6 +537,7 @@ export const promise = async ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),

View File

@@ -8,6 +8,7 @@ type PopulateArgs = {
data: Record<string, unknown>
dataReference: Record<string, any>
depth: number
draft: boolean
fallbackLocale: null | string
field: RelationshipField | UploadField
index?: number
@@ -23,6 +24,7 @@ const populate = async ({
data,
dataReference,
depth,
draft = false,
fallbackLocale,
field,
index,
@@ -62,6 +64,7 @@ const populate = async ({
fallbackLocale,
overrideAccess,
showHiddenFields,
draft,
]),
)
}
@@ -94,6 +97,7 @@ const populate = async ({
type PromiseArgs = {
currentDepth: number
depth: number
draft: boolean
fallbackLocale: null | string
field: RelationshipField | UploadField
locale: null | string
@@ -106,6 +110,7 @@ type PromiseArgs = {
const relationshipPopulationPromise = async ({
currentDepth,
depth,
draft,
fallbackLocale,
field,
locale,
@@ -133,6 +138,7 @@ const relationshipPopulationPromise = async ({
data: siblingDoc[field.name][key][index],
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
index,
@@ -156,6 +162,7 @@ const relationshipPopulationPromise = async ({
data: relatedDoc,
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
index,
@@ -182,6 +189,7 @@ const relationshipPopulationPromise = async ({
data: siblingDoc[field.name][key],
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
key,
@@ -201,6 +209,7 @@ const relationshipPopulationPromise = async ({
data: siblingDoc[field.name],
dataReference: resultingDoc,
depth: populateDepth,
draft,
fallbackLocale,
field,
locale,

View File

@@ -11,6 +11,7 @@ type Args = {
currentDepth: number
depth: number
doc: Record<string, unknown>
draft: boolean
fallbackLocale: null | string
fieldPromises: Promise<void>[]
fields: (Field | TabAsField)[]
@@ -33,6 +34,7 @@ export const traverseFields = ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
fieldPromises,
fields,
@@ -56,6 +58,7 @@ export const traverseFields = ({
currentDepth,
depth,
doc,
draft,
fallbackLocale,
field,
fieldPromises,

View File

@@ -100,6 +100,7 @@ async function findOne<T extends Record<string, unknown>>(args: Args): Promise<T
context: req.context,
depth,
doc,
draft: draftEnabled,
fallbackLocale,
global: globalConfig,
locale,

View File

@@ -108,6 +108,7 @@ async function findVersionByID<T extends TypeWithVersion<T> = any>(args: Argumen
currentDepth,
depth,
doc: result.version,
draft: undefined,
fallbackLocale,
global: globalConfig,
locale,

View File

@@ -97,6 +97,7 @@ async function findVersions<T extends TypeWithVersion<T>>(
// Patch globalType onto version doc
globalType: globalConfig.slug,
},
draft: undefined,
fallbackLocale,
findMany: true,
global: globalConfig,

View File

@@ -105,6 +105,7 @@ async function restoreVersion<T extends TypeWithVersion<T> = any>(args: Argument
context: req.context,
depth,
doc: result,
draft: undefined,
fallbackLocale,
global: globalConfig,
locale,

View File

@@ -97,6 +97,7 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
context: req.context,
depth: 0,
doc: globalJSON,
draft: draftArg,
fallbackLocale,
global: globalConfig,
locale,
@@ -220,6 +221,7 @@ async function update<TSlug extends keyof GeneratedTypes['globals']>(
context: req.context,
depth,
doc: result,
draft: draftArg,
fallbackLocale: null,
global: globalConfig,
locale,

View File

@@ -312,6 +312,7 @@ function buildObjectType({
type = type || newlyCreatedBlockType
const relationshipArgs: {
draft?: unknown
fallbackLocale?: unknown
limit?: unknown
locale?: unknown
@@ -319,6 +320,16 @@ function buildObjectType({
where?: unknown
} = {}
const relationsUseDrafts = (Array.isArray(relationTo) ? relationTo : [relationTo]).some(
(relation) => payload.collections[relation].config.versions?.drafts,
)
if (relationsUseDrafts) {
relationshipArgs.draft = {
type: GraphQLBoolean,
}
}
if (payload.config.localization) {
relationshipArgs.locale = {
type: payload.types.localeInputType,
@@ -330,6 +341,11 @@ function buildObjectType({
}
const relationship = {
type: withNullableType(
field,
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
forceNullable,
),
args: relationshipArgs,
extensions: { complexity: 10 },
async resolve(parent, args, context) {
@@ -337,6 +353,7 @@ function buildObjectType({
const locale = args.locale || context.req.locale
const fallbackLocale = args.fallbackLocale || context.req.fallbackLocale
let relatedCollectionSlug = field.relationTo
const draft = args.draft ?? context.req.query?.draft
if (hasManyValues) {
const results = []
@@ -362,6 +379,7 @@ function buildObjectType({
fallbackLocale,
false,
false,
draft,
]),
)
@@ -408,6 +426,7 @@ function buildObjectType({
fallbackLocale,
false,
false,
draft,
]),
)
@@ -430,11 +449,6 @@ function buildObjectType({
return null
},
type: withNullableType(
field,
hasManyValues ? new GraphQLList(new GraphQLNonNull(type)) : type,
forceNullable,
),
}
return {
@@ -445,6 +459,7 @@ function buildObjectType({
richText: (objectTypeConfig: ObjectTypeConfig, field: RichTextField) => ({
...objectTypeConfig,
[field.name]: {
type: withNullableType(field, GraphQLJSON, forceNullable),
args: {
depth: {
type: GraphQLInt,
@@ -464,6 +479,7 @@ function buildObjectType({
await editor?.populationPromise({
context,
depth,
draft: args.draft,
field,
findMany: false,
flattenLocales: false,
@@ -477,7 +493,6 @@ function buildObjectType({
return parent[field.name]
},
type: withNullableType(field, GraphQLJSON, forceNullable),
},
}),
row: (objectTypeConfig: ObjectTypeConfig, field: RowField) =>
@@ -586,6 +601,7 @@ function buildObjectType({
const relatedCollectionSlug = field.relationTo
const upload = {
type,
args: uploadArgs,
extensions: { complexity: 20 },
async resolve(parent, args, context) {
@@ -614,7 +630,6 @@ function buildObjectType({
return null
},
type,
}
const whereFields = payload.collections[relationTo].config.fields

View File

@@ -5,7 +5,7 @@
"accountOfCurrentUser": "Účet současného uživatele",
"alreadyActivated": "Již aktivováno",
"alreadyLoggedIn": "Již přihlášen",
"apiKey": "Klíč API",
"apiKey": "API klíč",
"backToLogin": "Zpět na přihlášení",
"beginCreateFirstUser": "Začněte vytvořením svého prvního uživatele.",
"changePassword": "Změnit heslo",
@@ -15,15 +15,15 @@
"createFirstUser": "Vytvořit prvního uživatele",
"emailNotValid": "Zadaný email není platný",
"emailSent": "Email odeslán",
"enableAPIKey": "Povolit klíč API",
"enableAPIKey": "Povolit API klíč",
"failedToUnlock": "Nepodařilo se odemknout",
"forceUnlock": "Vynutit odemčení",
"forgotPassword": "Zapomněli jste heslo?",
"forgotPasswordEmailInstructions": "Zadejte svůj email níže. Obdržíte email s instrukcemi, jak resetovat vaše heslo.",
"forgotPasswordQuestion": "Zapomněli jste heslo?",
"generate": "Generovat",
"generateNewAPIKey": "Generovat nový klíč API",
"generatingNewAPIKeyWillInvalidate": "Vytvoření nového klíče API <1>zneplatní</1> předchozí klíč. Opravdu chcete pokračovat?",
"generateNewAPIKey": "Generovat nový API klíč",
"generatingNewAPIKeyWillInvalidate": "Vytvoření nového API klíče <1>zneplatní</1> předchozí klíč. Opravdu chcete pokračovat?",
"lockUntil": "Uzamknout do",
"logBackIn": "Znovu se přihlásit",
"logOut": "Odhlásit se",
@@ -37,7 +37,7 @@
"loginWithAnotherUser": "Abyste se mohli přihlásit s jiným uživatelem, nejdříve se <0>odhlaste</0>.",
"logout": "Odhlásit se",
"logoutUser": "Odhlásit uživatele",
"newAPIKeyGenerated": "Byl vygenerován nový klíč API.",
"newAPIKeyGenerated": "Byl vygenerován nový API klíč.",
"newAccountCreated": "Pro přístup k <a href=\"{{serverURL}}\">{{serverURL}}</a> byl pro vás vytvořen nový účet. Klepněte na následující odkaz nebo zkopírujte URL do svého prohlížeče pro ověření vašeho emailu: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Po ověření vašeho emailu se budete moci úspěšně přihlásit.",
"newPassword": "Nové heslo",
"resetPassword": "Resetovat heslo",
@@ -96,7 +96,7 @@
},
"fields": {
"addLabel": "Přidat {{label}}",
"addLink": "Přidat Odkaz",
"addLink": "Přidat odkaz",
"addNew": "Přidat nový",
"addNewLabel": "Přidat nový {{label}}",
"addRelationship": "Přidat vztah",
@@ -147,10 +147,10 @@
"addBelow": "Přidat pod",
"addFilter": "Přidat filtr",
"adminTheme": "Motiv administračního rozhraní",
"and": "A",
"and": "a",
"applyChanges": "Použít změny",
"ascending": "Vzestupně",
"automatic": "Automatické",
"automatic": "Automatický",
"backToDashboard": "Zpět na nástěnku",
"cancel": "Zrušit",
"changesNotSaved": "Vaše změny nebyly uloženy. Pokud teď odejdete, ztratíte své změny.",
@@ -171,7 +171,7 @@
"createdAt": "Vytvořeno v",
"creating": "Vytváření",
"creatingNewLabel": "Vytváření nového {{label}}",
"dark": "Tmavé",
"dark": "Tmavý",
"dashboard": "Nástěnka",
"delete": "Odstranit",
"deletedCountSuccessfully": "Úspěšně smazáno {{count}} {{label}}.",
@@ -185,7 +185,7 @@
"duplicateWithoutSaving": "Duplikovat bez uložení změn",
"edit": "Upravit",
"editLabel": "Upravit {{label}}",
"editing": "Úpravy",
"editing": "Úprava",
"editingLabel_many": "Úprava {{count}} {{label}}",
"editingLabel_one": "Úprava {{count}} {{label}}",
"editingLabel_other": "Úprava {{count}} {{label}}",
@@ -203,7 +203,7 @@
"lastModified": "Naposledy změněno",
"leaveAnyway": "Přesto odejít",
"leaveWithoutSaving": "Odejít bez uložení",
"light": "Světlé",
"light": "Světlý",
"livePreview": "Náhled",
"loading": "Načítání",
"locale": "Místní verze",
@@ -226,7 +226,7 @@
"order": "Pořadí",
"pageNotFound": "Stránka nenalezena",
"password": "Heslo",
"payloadSettings": "Nastavení datového záběru",
"payloadSettings": "Nastavení Payload",
"perPage": "Na stránku: {{limit}}",
"remove": "Odstranit",
"reset": "Resetovat",
@@ -274,19 +274,19 @@
"isLessThanOrEqualTo": "je menší nebo rovno",
"isLike": "je jako",
"isNotEqualTo": "není rovno",
"isNotIn": "není in",
"isNotIn": "není v",
"near": "blízko"
},
"upload": {
"crop": "Plodina",
"cropToolDescription": "Přetáhněte rohy vybrané oblasti, nakreslete novou oblast nebo upravte hodnoty níže.",
"crop": "Ořez",
"cropToolDescription": "Přetáhněte rohy vybrané oblasti, nakreslete novou oblast nebo upravte níže uvedené hodnoty.",
"dragAndDrop": "Přetáhněte soubor",
"dragAndDropHere": "nebo sem přetáhněte soubor",
"editImage": "Upravit obrázek",
"fileName": "Název souboru",
"fileSize": "Velikost souboru",
"focalPoint": "Středobod",
"focalPointDescription": "Přetáhněte bod zaměření přímo na náhled nebo upravte hodnoty níže.",
"focalPointDescription": "Přetáhněte bod zaměření přímo na náhled nebo upravte níže uvedené hodnoty.",
"height": "Výška",
"lessInfo": "Méně informací",
"moreInfo": "Více informací",
@@ -294,7 +294,7 @@
"selectCollectionToBrowse": "Vyberte kolekci pro procházení",
"selectFile": "Vyberte soubor",
"setCropArea": "Nastavit oblast ořezu",
"setFocalPoint": "Nastavit ohnisko",
"setFocalPoint": "Nastavit středobod",
"sizes": "Velikosti",
"sizesFor": "Velikosti pro {{label}}",
"width": "Šířka"

View File

@@ -1,7 +1,7 @@
{
"name": "@payloadcms/plugin-cloud",
"description": "The official Payload Cloud plugin",
"version": "3.0.0",
"version": "3.0.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",

View File

@@ -8,8 +8,14 @@ interface Args {
endpoint: string
}
type GenericUpload = {
id: string
sizes?: Record<string, { url?: string }>
url?: string
}
export const getCacheUploadsAfterChangeHook =
({ endpoint }: Args): CollectionAfterChangeHook =>
({ endpoint }: Args): CollectionAfterChangeHook<GenericUpload> =>
async ({ doc, operation, req }) => {
if (!req || !process.env.PAYLOAD_CLOUD_CACHE_KEY) return doc
@@ -37,7 +43,7 @@ export const getCacheUploadsAfterDeleteHook =
}
type PurgeRequest = {
doc: any
doc: GenericUpload
endpoint: string
operation: string
req: PayloadRequest
@@ -56,21 +62,30 @@ async function purge({ doc, endpoint, operation, req }: PurgeRequest) {
return
}
const body = {
cacheKey: process.env.PAYLOAD_CLOUD_CACHE_KEY,
filepath: doc.url,
projectID: process.env.PAYLOAD_CLOUD_PROJECT_ID,
}
req.payload.logger.debug({
filepath: doc.url,
msg: 'Attempting to purge cache',
operation,
project: {
id: process.env.PAYLOAD_CLOUD_PROJECT_ID,
},
})
const filepaths = [filePath]
try {
if (doc.sizes && Object.keys(doc.sizes).length) {
const urls = Object.values(doc.sizes)
.map((size) => size?.url)
.filter(Boolean)
filepaths.push(...urls)
}
const body = {
cacheKey: process.env.PAYLOAD_CLOUD_CACHE_KEY,
filepaths,
projectID: process.env.PAYLOAD_CLOUD_PROJECT_ID,
}
req.payload.logger.debug({
filepaths,
msg: 'Purging cache for filepaths',
operation,
project: {
id: process.env.PAYLOAD_CLOUD_PROJECT_ID,
},
})
const purgeRes = await fetch(`${endpoint}/api/purge-cache`, {
body: JSON.stringify({
...body,
@@ -87,6 +102,10 @@ async function purge({ doc, endpoint, operation, req }: PurgeRequest) {
statusCode: purgeRes.status,
})
} catch (err: unknown) {
req.payload.logger.error({ body, err, msg: '/purge-cache call failed' })
req.payload.logger.error({
data: { id: doc.id, filepaths },
err,
msg: '/purge-cache call failed',
})
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-lexical",
"version": "0.9.1",
"version": "0.9.3",
"description": "The officially supported Lexical richtext adapter for Payload",
"repository": {
"type": "git",

View File

@@ -59,6 +59,7 @@ export const blockPopulationPromiseHOC = (
currentDepth,
data: blockFieldData,
depth,
draft: false,
editorPopulationPromises,
fields: block.fields,
findMany,

View File

@@ -39,6 +39,7 @@ export const linkPopulationPromiseHOC = (
currentDepth,
data: node?.fields?.doc,
depth,
draft: false,
field,
key: 'value',
overrideAccess,
@@ -54,6 +55,7 @@ export const linkPopulationPromiseHOC = (
currentDepth,
data: node.fields || {},
depth,
draft: false,
editorPopulationPromises,
fields: props.fields,
findMany,

View File

@@ -6,6 +6,7 @@ import { populate } from '../../../populate/populate'
export const relationshipPopulationPromise: PopulationPromise<SerializedRelationshipNode> = ({
currentDepth,
depth,
draft,
field,
node,
overrideAccess,
@@ -25,6 +26,7 @@ export const relationshipPopulationPromise: PopulationPromise<SerializedRelation
currentDepth,
data: node,
depth,
draft,
field,
key: 'value',
overrideAccess,

View File

@@ -36,6 +36,7 @@ export const uploadPopulationPromiseHOC = (
currentDepth,
data: node,
depth,
draft: false,
field,
key: 'value',
overrideAccess,
@@ -50,6 +51,7 @@ export const uploadPopulationPromiseHOC = (
currentDepth,
data: node.fields || {},
depth,
draft: false,
editorPopulationPromises,
fields: props?.collections?.[node?.relationTo]?.fields,
findMany,

View File

@@ -32,6 +32,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
context: RequestContext
currentDepth: number
depth: number
draft: boolean
/**
* This maps all population promises to the node type
*/

View File

@@ -36,7 +36,7 @@ export const LexicalEditor: React.FC<Pick<LexicalProviderProps, 'editorConfig' |
useEffect(() => {
const updateViewPortWidth = () => {
const isNextSmallWidthViewport = window.matchMedia('(max-width: 1025px)').matches
const isNextSmallWidthViewport = window.matchMedia('(max-width: 768px)').matches
if (isNextSmallWidthViewport !== isSmallWidthViewport) {
setIsSmallWidthViewport(isNextSmallWidthViewport)

View File

@@ -147,36 +147,36 @@ function FloatingSelectToolbar({
anchorElem: HTMLElement
editor: LexicalEditor
}): JSX.Element {
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null)
const floatingToolbarRef = useRef<HTMLDivElement | null>(null)
const caretRef = useRef<HTMLDivElement | null>(null)
const { editorConfig } = useEditorConfigContext()
const closeFloatingToolbar = useCallback(() => {
if (popupCharStylesEditorRef?.current) {
const isOpacityZero = popupCharStylesEditorRef.current.style.opacity === '0'
const isPointerEventsNone = popupCharStylesEditorRef.current.style.pointerEvents === 'none'
if (floatingToolbarRef?.current) {
const isOpacityZero = floatingToolbarRef.current.style.opacity === '0'
const isPointerEventsNone = floatingToolbarRef.current.style.pointerEvents === 'none'
if (!isOpacityZero) {
popupCharStylesEditorRef.current.style.opacity = '0'
floatingToolbarRef.current.style.opacity = '0'
}
if (!isPointerEventsNone) {
popupCharStylesEditorRef.current.style.pointerEvents = 'none'
floatingToolbarRef.current.style.pointerEvents = 'none'
}
}
}, [popupCharStylesEditorRef])
}, [floatingToolbarRef])
const mouseMoveListener = useCallback(
(e: MouseEvent) => {
if (popupCharStylesEditorRef?.current && (e.buttons === 1 || e.buttons === 3)) {
const isOpacityZero = popupCharStylesEditorRef.current.style.opacity === '0'
const isPointerEventsNone = popupCharStylesEditorRef.current.style.pointerEvents === 'none'
if (floatingToolbarRef?.current && (e.buttons === 1 || e.buttons === 3)) {
const isOpacityZero = floatingToolbarRef.current.style.opacity === '0'
const isPointerEventsNone = floatingToolbarRef.current.style.pointerEvents === 'none'
if (!isOpacityZero || !isPointerEventsNone) {
// Check if the mouse is not over the popup
const x = e.clientX
const y = e.clientY
const elementUnderMouse = document.elementFromPoint(x, y)
if (!popupCharStylesEditorRef.current.contains(elementUnderMouse)) {
if (!floatingToolbarRef.current.contains(elementUnderMouse)) {
// Mouse is not over the target element => not a normal click, but probably a drag
closeFloatingToolbar()
}
@@ -187,15 +187,15 @@ function FloatingSelectToolbar({
)
const mouseUpListener = useCallback(() => {
if (popupCharStylesEditorRef?.current) {
if (popupCharStylesEditorRef.current.style.opacity !== '1') {
popupCharStylesEditorRef.current.style.opacity = '1'
if (floatingToolbarRef?.current) {
if (floatingToolbarRef.current.style.opacity !== '1') {
floatingToolbarRef.current.style.opacity = '1'
}
if (popupCharStylesEditorRef.current.style.pointerEvents !== 'auto') {
popupCharStylesEditorRef.current.style.pointerEvents = 'auto'
if (floatingToolbarRef.current.style.pointerEvents !== 'auto') {
floatingToolbarRef.current.style.pointerEvents = 'auto'
}
}
}, [popupCharStylesEditorRef])
}, [floatingToolbarRef])
useEffect(() => {
document.addEventListener('mousemove', mouseMoveListener)
@@ -205,15 +205,14 @@ function FloatingSelectToolbar({
document.removeEventListener('mousemove', mouseMoveListener)
document.removeEventListener('mouseup', mouseUpListener)
}
}, [popupCharStylesEditorRef, mouseMoveListener, mouseUpListener])
}, [floatingToolbarRef, mouseMoveListener, mouseUpListener])
const updateTextFormatFloatingToolbar = useCallback(() => {
const selection = $getSelection()
const popupCharStylesEditorElem = popupCharStylesEditorRef.current
const nativeSelection = window.getSelection()
if (popupCharStylesEditorElem === null) {
if (floatingToolbarRef.current === null) {
return
}
@@ -227,17 +226,25 @@ function FloatingSelectToolbar({
) {
const rangeRect = getDOMRangeRect(nativeSelection, rootElement)
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem, 'center')
// Position floating toolbar
const offsetIfFlipped = setFloatingElemPosition(
rangeRect, // selection to position around
floatingToolbarRef.current, // what to position
anchorElem, // anchor elem
'center',
)
// Position caret
if (caretRef.current) {
setFloatingElemPosition(
rangeRect, // selection to position around
caretRef.current, // what to position
popupCharStylesEditorElem, // anchor elem
floatingToolbarRef.current, // anchor elem
'center',
10,
5,
true,
offsetIfFlipped,
)
}
}
@@ -288,7 +295,7 @@ function FloatingSelectToolbar({
}, [editor, updateTextFormatFloatingToolbar])
return (
<div className="floating-select-toolbar-popup" ref={popupCharStylesEditorRef}>
<div className="floating-select-toolbar-popup" ref={floatingToolbarRef}>
<div className="caret" ref={caretRef} />
{editor.isEditable() && (
<React.Fragment>

View File

@@ -1,9 +1,10 @@
const VERTICAL_GAP = 10
const HORIZONTAL_OFFSET = 5
// TODO: This works fine with some dirty fixes (looking at you, specialHandlingForCaret). But this definitely needs refactoring and documentation, to be easier to understand and maintain.
// TODO: needs refactoring
// This is supposed to position the floatingElem based on the parent (anchorElem) and the target (targetRect) which is usually the selected text.
// So basically, it positions the floatingElem either below or above the target (targetRect) and aligns it to the left or center of the target (targetRect).
// This is used for positioning the floating toolbar (anchor: richtext editor) and its caret (anchor: floating toolbar)
export function setFloatingElemPosition(
targetRect: ClientRect | null,
floatingElem: HTMLElement,
@@ -12,7 +13,9 @@ export function setFloatingElemPosition(
verticalGap: number = VERTICAL_GAP,
horizontalOffset: number = HORIZONTAL_OFFSET,
specialHandlingForCaret = false,
): void {
anchorFlippedOffset = 0, // Offset which was added to the anchor (for caret, floating toolbar) if it was flipped
): number {
// Returns the top offset if the target was flipped
const scrollerElem = anchorElem.parentElement
if (targetRect === null || scrollerElem == null) {
@@ -33,8 +36,11 @@ export function setFloatingElemPosition(
left = targetRect.left + targetRect.width / 2 - floatingElemRect.width / 2
}
if (top < editorScrollerRect.top) {
top += floatingElemRect.height + targetRect.height + verticalGap * 2
let addedToTop = 0
if (top < editorScrollerRect.top && !specialHandlingForCaret) {
addedToTop = floatingElemRect.height + targetRect.height + verticalGap * 2
top += addedToTop
}
if (horizontalPosition === 'center') {
@@ -49,22 +55,21 @@ export function setFloatingElemPosition(
}
}
top -= anchorElementRect.top
left -= anchorElementRect.left
floatingElem.style.opacity = '1'
if (specialHandlingForCaret && top == 0 /* 0 Happens when selecting 1st line */) {
top -= 44 // Especially this arbitrary number needs refactoring (this is for the caret)
// rotate too
floatingElem.style.transform = `translate(${left}px, ${top}px) rotate(180deg)`
} else if (
specialHandlingForCaret &&
top === -63 /* -63 Happens when selecting 2nd line in multi-line paragraph */
) {
top += 18 // Especially this arbitrary number needs refactoring (this is for the caret)
if (specialHandlingForCaret && anchorFlippedOffset !== 0) {
// Floating select toolbar was flipped (positioned below text rather than above). Thus, the caret now needs to be positioned
// on the other side and rotated.
top -= anchorElementRect.bottom - anchorFlippedOffset + floatingElemRect.height - 3
// top += anchorFlippedOffset - anchorElementRect.height - floatingElemRect.height + 2
floatingElem.style.transform = `translate(${left}px, ${top}px) rotate(180deg)`
} else {
top -= anchorElementRect.top
floatingElem.style.transform = `translate(${left}px, ${top}px)`
}
return addedToTop
}

View File

@@ -182,6 +182,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
context,
currentDepth,
depth,
draft,
field,
findMany,
flattenLocales,
@@ -197,6 +198,7 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
context,
currentDepth,
depth,
draft,
editorPopulationPromises: finalSanitizedEditorConfig.features.populationPromises,
field,
findMany,

View File

@@ -8,6 +8,7 @@ type Arguments = {
currentDepth?: number
data: unknown
depth: number
draft: boolean
field: RichTextField<SerializedEditorState, AdapterProps>
key: number | string
overrideAccess?: boolean
@@ -21,6 +22,7 @@ export const populate = async ({
currentDepth,
data,
depth,
draft,
key,
overrideAccess,
req,
@@ -43,6 +45,7 @@ export const populate = async ({
req.fallbackLocale,
typeof overrideAccess === 'undefined' ? false : overrideAccess,
showHiddenFields,
draft,
]),
)

View File

@@ -10,6 +10,7 @@ type NestedRichTextFieldsArgs = {
currentDepth?: number
data: unknown
depth: number
draft: boolean
/**
* This maps all the population promises to the node types
*/
@@ -30,6 +31,7 @@ export const recurseNestedFields = ({
currentDepth = 0,
data,
depth,
draft,
fields,
findMany,
flattenLocales,
@@ -46,6 +48,7 @@ export const recurseNestedFields = ({
currentDepth,
depth,
doc: data as any, // Looks like it's only needed for hooks and access control, so doesn't matter what we pass here right now
draft,
fallbackLocale: req.fallbackLocale,
fieldPromises: promises, // Not sure if what I pass in here makes sense. But it doesn't seem like it's used at all anyways
fields,

View File

@@ -28,6 +28,7 @@ export const recurseRichText = ({
context,
currentDepth = 0,
depth,
draft,
editorPopulationPromises,
field,
findMany,
@@ -52,11 +53,12 @@ export const recurseRichText = ({
context,
currentDepth,
depth,
draft,
editorPopulationPromises,
field,
findMany,
flattenLocales,
node: node,
node,
overrideAccess,
populationPromises,
req,
@@ -73,6 +75,7 @@ export const recurseRichText = ({
context,
currentDepth,
depth,
draft,
editorPopulationPromises,
field,
findMany,
@@ -93,6 +96,7 @@ export const richTextRelationshipPromise = async ({
context,
currentDepth,
depth,
draft,
editorPopulationPromises,
field,
findMany,
@@ -110,6 +114,7 @@ export const richTextRelationshipPromise = async ({
context,
currentDepth,
depth,
draft,
editorPopulationPromises,
field,
findMany,

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/richtext-slate",
"version": "1.5.0",
"version": "1.5.2",
"description": "The officially supported Slate richtext adapter for Payload",
"repository": {
"type": "git",

View File

@@ -7,6 +7,7 @@ type Arguments = {
currentDepth?: number
data: unknown
depth: number
draft: boolean
field: RichTextField<any[], AdapterArguments, AdapterArguments>
key: number | string
overrideAccess?: boolean
@@ -20,6 +21,7 @@ export const populate = async ({
currentDepth,
data,
depth,
draft,
key,
overrideAccess,
req,
@@ -42,6 +44,7 @@ export const populate = async ({
req.fallbackLocale,
typeof overrideAccess === 'undefined' ? false : overrideAccess,
showHiddenFields,
draft,
]),
)

View File

@@ -9,6 +9,7 @@ type NestedRichTextFieldsArgs = {
currentDepth?: number
data: unknown
depth: number
draft: boolean
fields: Field[]
overrideAccess: boolean
promises: Promise<void>[]
@@ -20,6 +21,7 @@ export const recurseNestedFields = ({
currentDepth = 0,
data,
depth,
draft,
fields,
overrideAccess = false,
promises,
@@ -41,6 +43,7 @@ export const recurseNestedFields = ({
currentDepth,
data: data[field.name],
depth,
draft,
field,
key: i,
overrideAccess,
@@ -61,6 +64,7 @@ export const recurseNestedFields = ({
currentDepth,
data: data[field.name],
depth,
draft,
field,
key: i,
overrideAccess,
@@ -85,6 +89,7 @@ export const recurseNestedFields = ({
currentDepth,
data: data[field.name],
depth,
draft,
field,
key: 'value',
overrideAccess,
@@ -104,6 +109,7 @@ export const recurseNestedFields = ({
currentDepth,
data,
depth,
draft,
field,
key: field.name,
overrideAccess,
@@ -118,6 +124,7 @@ export const recurseNestedFields = ({
currentDepth,
data: data[field.name],
depth,
draft,
fields: field.fields,
overrideAccess,
promises,
@@ -129,6 +136,7 @@ export const recurseNestedFields = ({
currentDepth,
data,
depth,
draft,
fields: field.fields,
overrideAccess,
promises,
@@ -142,6 +150,7 @@ export const recurseNestedFields = ({
currentDepth,
data,
depth,
draft,
fields: tab.fields,
overrideAccess,
promises,
@@ -158,6 +167,7 @@ export const recurseNestedFields = ({
currentDepth,
data: data[field.name][i],
depth,
draft,
fields: block.fields,
overrideAccess,
promises,
@@ -174,6 +184,7 @@ export const recurseNestedFields = ({
currentDepth,
data: data[field.name][i],
depth,
draft,
fields: field.fields,
overrideAccess,
promises,
@@ -191,6 +202,7 @@ export const recurseNestedFields = ({
children: node.children,
currentDepth,
depth,
draft,
field,
overrideAccess,
promises,

View File

@@ -11,6 +11,7 @@ type RecurseRichTextArgs = {
children: unknown[]
currentDepth: number
depth: number
draft: boolean
field: RichTextField<any[], AdapterArguments, AdapterArguments>
overrideAccess: boolean
promises: Promise<void>[]
@@ -22,6 +23,7 @@ export const recurseRichText = ({
children,
currentDepth = 0,
depth,
draft,
field,
overrideAccess = false,
promises,
@@ -45,6 +47,7 @@ export const recurseRichText = ({
currentDepth,
data: element,
depth,
draft,
field,
key: 'value',
overrideAccess,
@@ -61,6 +64,7 @@ export const recurseRichText = ({
currentDepth,
data: element.fields || {},
depth,
draft,
fields: field.admin.upload.collections[element.relationTo].fields,
overrideAccess,
promises,
@@ -82,6 +86,7 @@ export const recurseRichText = ({
currentDepth,
data: element.doc,
depth,
draft,
field,
key: 'value',
overrideAccess,
@@ -97,6 +102,7 @@ export const recurseRichText = ({
currentDepth,
data: element.fields || {},
depth,
draft,
fields: field.admin?.link?.fields,
overrideAccess,
promises,
@@ -111,6 +117,7 @@ export const recurseRichText = ({
children: element.children,
currentDepth,
depth,
draft,
field,
overrideAccess,
promises,
@@ -125,6 +132,7 @@ export const recurseRichText = ({
export const richTextRelationshipPromise = async ({
currentDepth,
depth,
draft,
field,
overrideAccess,
req,
@@ -137,6 +145,7 @@ export const richTextRelationshipPromise = async ({
children: siblingDoc[field.name] as unknown[],
currentDepth,
depth,
draft,
field,
overrideAccess,
promises,

View File

@@ -21,16 +21,17 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
}),
outputSchema: ({ isRequired }) => {
return {
type: withNullableJSONSchemaType('array', isRequired),
items: {
type: 'object',
},
type: withNullableJSONSchemaType('array', isRequired),
}
},
populationPromise({
context,
currentDepth,
depth,
draft,
field,
findMany,
flattenLocales,
@@ -50,6 +51,7 @@ export function slateEditor(args: AdapterArguments): RichTextAdapter<any[], Adap
context,
currentDepth,
depth,
draft,
field,
findMany,
flattenLocales,

View File

@@ -1,5 +1,8 @@
import type { PlaywrightTestConfig } from '@playwright/test'
export const EXPECT_TIMEOUT = 45000
export const POLL_TOPASS_TIMEOUT = EXPECT_TIMEOUT * 4 // That way expect.poll() or expect().toPass can retry 4 times. 4x higher than default expect timeout => can retry 4 times if retryable expects are used inside
const config: PlaywrightTestConfig = {
// Look for test files in the "test" directory, relative to this configuration file
testDir: 'test',

View File

@@ -38,6 +38,10 @@ export const pointSlug = 'point'
export const errorOnHookSlug = 'error-on-hooks'
export default buildConfigWithDefaults({
localization: {
defaultLocale: 'en',
locales: ['en', 'es'],
},
collections: [
{
access: openAccess,
@@ -335,6 +339,30 @@ export default buildConfigWithDefaults({
],
slug: 'content-type',
},
{
slug: 'cyclical-relationship',
access: openAccess,
fields: [
{
name: 'title',
type: 'text',
localized: true,
},
{
name: 'relationToSelf',
type: 'relationship',
relationTo: 'cyclical-relationship',
},
{
name: 'relationToSelfPoly',
type: 'relationship',
relationTo: ['cyclical-relationship'],
},
],
versions: {
drafts: true,
},
},
],
graphQL: {
queries: (GraphQL) => {

View File

@@ -871,6 +871,80 @@ describe('collections-graphql', () => {
expect(docs[0].relationHasManyField).toHaveLength(0)
})
it('should query relationships with locale', async () => {
const newDoc = await payload.create({
collection: 'cyclical-relationship',
data: {
title: {
en: 'English title',
es: 'Spanish title',
},
},
locale: '*',
})
await payload.update({
collection: 'cyclical-relationship',
id: newDoc.id,
data: {
relationToSelf: newDoc.id,
},
})
const query = `query($locale: LocaleInputType) {
CyclicalRelationships(locale: $locale) {
docs {
title
relationToSelf {
title
}
}
}
}`
const response = (await client.request(query, { locale: 'es' })) as any
const queriedDoc = response.CyclicalRelationships.docs[0]
expect(queriedDoc.title).toEqual(queriedDoc.relationToSelf.title)
})
it('should query correctly with draft argument', async () => {
// publish doc
const newDoc = await payload.create({
collection: 'cyclical-relationship',
draft: false,
data: {
title: '1',
},
})
// save new version
await payload.update({
collection: 'cyclical-relationship',
id: newDoc.id,
draft: true,
data: {
title: '2',
relationToSelf: newDoc.id,
},
})
const query = `{
CyclicalRelationships(draft: true) {
docs {
title
relationToSelf(draft: false) {
title
}
}
}
}`
const response = (await client.request(query)) as any
const queriedDoc = response.CyclicalRelationships.docs[0]
expect(queriedDoc.title).toEqual('2')
expect(queriedDoc.relationToSelf.title).toEqual('1')
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -358,6 +358,65 @@ describe('fields - relationship', () => {
await expect(options).toContainText('truth')
})
test('should use filterOptions in the list view where builder', async () => {
const relationOneDoc = await payload.create({
collection: relationOneSlug,
data: {
name: 'include',
},
})
const relationOneMiss = await payload.create({
collection: relationOneSlug,
data: {
name: 'ignored',
},
})
await page.goto(url.list)
// expand the filter options
await page.locator('.list-controls__toggle-where').click()
// click add filter
await page.locator('.where-builder__add-first-filter').click()
// select the "Relationship Many Filtered" field
await page.locator('.condition__field .rs__control').click()
const options = page.locator('.rs__option')
await options.locator('text=Relationship Many Filtered').click()
// select the equals operator
await page.locator('.condition__operator .rs__control').click()
const operatorOptions = page.locator('.rs__option')
await operatorOptions.locator('text=equals').click()
// open the value dropdown
await page.locator('.condition__value .rs__control').click()
// expect that relation-one has the option of the document id
const valueOptions = page.locator('.rs__option')
await expect(valueOptions.locator(`text=${relationOneDoc.id}`)).toHaveCount(1)
await expect(valueOptions.locator(`text=${relationOneMiss.id}`)).toHaveCount(0)
// enter something in search
await page
.locator('.condition__value .rs__input')
.fill(relationOneDoc.id.toString().slice(0, 5))
// remove focus
await page.locator('.condition__value .rs__input').blur()
// open the value dropdown
await page.locator('.condition__value .rs__control').click()
// expect that relation-one has the option of the document id
const valueOptionsWithSearch = page.locator('.rs__option')
await expect(valueOptionsWithSearch.locator(`text=${relationOneDoc.id}`)).toHaveCount(1)
await expect(valueOptionsWithSearch.locator(`text=${relationOneMiss.id}`)).toHaveCount(0)
})
test('should open document drawer from read-only relationships', async () => {
await page.goto(url.edit(docWithExistingRelations.id))

View File

@@ -155,6 +155,21 @@ const ArrayFields: CollectionConfig = {
minRows: 2,
type: 'array',
},
{
name: 'disableSortItems',
defaultValue: arrayDefaultValue,
admin: {
isSortable: false,
},
fields: [
{
name: 'text',
required: true,
type: 'text',
},
],
type: 'array',
},
],
slug: arrayFieldsSlug,
versions: true,

View File

@@ -63,4 +63,15 @@ export const anotherArrayDoc: Partial<ArrayField> = {
text: 'second row',
},
],
disableSortItems: [
{
text: 'un-sortable item 1',
},
{
text: 'un-sortable item 2',
},
{
text: 'un-sortable item 3',
},
],
}

View File

@@ -124,6 +124,7 @@ const BlockFields: CollectionConfig = {
name: 'collapsedByDefaultBlocks',
admin: {
initCollapsed: true,
isSortable: false,
},
localized: true,
},

View File

@@ -696,6 +696,12 @@ describe('fields', () => {
})
})
})
test('should have disabled admin sorting', async () => {
await page.goto(url.create)
const field = page.locator('#field-collapsedByDefaultBlocks .array-actions__action-chevron')
expect(await field.count()).toEqual(0)
})
})
describe('array', () => {
@@ -716,6 +722,12 @@ describe('fields', () => {
await expect(field).toHaveValue('defaultValue')
})
test('should have disabled admin sorting', async () => {
await page.goto(url.create)
const field = page.locator('#field-disableSortItems .array-actions__action-chevron')
expect(await field.count()).toEqual(0)
})
test('should render RowLabel using a function', async () => {
const label = 'custom row label as function'
await page.goto(url.create)

View File

@@ -265,6 +265,10 @@ export interface ArrayField {
id?: string | null
}[]
| null
disableSortItems?: {
text: string
id?: string | null
}[]
updatedAt: string
createdAt: string
}

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