Merge branch 'master' into feat/db-adapters
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,5 +1,21 @@
|
||||
|
||||
|
||||
# [1.13.0](https://github.com/payloadcms/payload/compare/v1.12.0...v1.13.0) (2023-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* `setPreference()` return type ([#3125](https://github.com/payloadcms/payload/issues/3125)) ([463d6bb](https://github.com/payloadcms/payload/commit/463d6bbec66e61523bae3869df88bd98e7617390))
|
||||
* absolute staticURL admin thumbnails ([#3135](https://github.com/payloadcms/payload/issues/3135)) ([1039f39](https://github.com/payloadcms/payload/commit/1039f39c09260537616b22228080466e8df6e981))
|
||||
* adding and replacing similarly shaped block configs ([#3140](https://github.com/payloadcms/payload/issues/3140)) ([8e188cf](https://github.com/payloadcms/payload/commit/8e188cfe61db808c94d726967affdadf2e5abb9f))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* default tab labels from name ([#3129](https://github.com/payloadcms/payload/issues/3129)) ([e8f0516](https://github.com/payloadcms/payload/commit/e8f05165eb3a28c00deb11931db01ad1f8c75c74))
|
||||
* radio and select fields are filterable by options ([#3136](https://github.com/payloadcms/payload/issues/3136)) ([b117e73](https://github.com/payloadcms/payload/commit/b117e7346434bfc8edbfa92f5db45f63c57bab08))
|
||||
* recursive saveToJWT field support ([#3130](https://github.com/payloadcms/payload/issues/3130)) ([c6e0908](https://github.com/payloadcms/payload/commit/c6e09080767dad2ab8128ba330b2b344bb25ac6f))
|
||||
|
||||
# [1.12.0](https://github.com/payloadcms/payload/compare/v1.11.8...v1.12.0) (2023-08-04)
|
||||
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ Once enabled, each document that is created within the Collection can be thought
|
||||
|
||||
Successfully logging in returns a `JWT` (JSON web token) which is how a user will identify themselves to Payload. By providing this JWT via either an HTTP-only cookie or an `Authorization` header, Payload will automatically identify the user and add its user JWT data to the Express `req`, which is available throughout Payload including within access control, hooks, and more.
|
||||
|
||||
You can specify what data gets encoded to the JWT token by setting `saveToJWT` to true in your auth collection fields. If you wish to use a different key other than the field `name`, you can provide it to `saveToJWT` as a string.
|
||||
You can specify what data gets encoded to the JWT token by setting `saveToJWT` to true in your auth collection fields. If you wish to use a different key other than the field `name`, you can provide it to `saveToJWT` as a string. It is also possible to use `saveToJWT` on fields that are nested in inside groups and tabs. If a group has a `saveToJWT` set it will include the object with all sub-fields in the token. You can set `saveToJWT: false` for any fields you wish to omit. If a field inside a group has `saveToJWT` set, but the group does not, the field will be included at the top level of the token.
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong><br/>
|
||||
|
||||
@@ -12,15 +12,19 @@ keywords: array, fields, config, configuration, documentation, Content Managemen
|
||||
Its uses can be simple or highly complex.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/array.png"
|
||||
srcDark="https://payloadcms.com/images/docs/fields/array-dark.png"
|
||||
alt="Array field with two Rows in Payload admin panel"
|
||||
caption="Admin panel screenshot of an Array field with two Rows"
|
||||
/>
|
||||
|
||||
**Example uses:**
|
||||
|
||||
- A "slider" with an image ([upload field](/docs/fields/upload)) and a caption ([text field](/docs/fields/text))
|
||||
- Navigational structures where editors can specify nav items containing pages ([relationship field](/docs/fields/relationship)), an "open in new tab" [checkbox field](/docs/fields/checkbox)
|
||||
- Event agenda "timeslots" where you need to specify start & end time ([date field](/docs/fields/date)), label ([text field](/docs/fields/text)), and Learn More page [relationship](/docs/fields/relationship)
|
||||
|
||||

|
||||
_Admin panel screenshot of an Array field with a Row containing two text fields, a read-only text field and a checkbox_
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -13,15 +13,19 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
|
||||
match the shape of one of your provided block configs.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/blocks.png"
|
||||
srcDark="https://payloadcms.com/images/docs/fields/blocks-dark.png"
|
||||
alt="Admin panel screenshot of add Blocks drawer view"
|
||||
caption="Admin panel screenshot of add Blocks drawer view"
|
||||
/>
|
||||
|
||||
**Example uses:**
|
||||
|
||||
- A layout builder tool that grants editors to design highly customizable page or post layouts. Blocks could include configs such as `Quote`, `CallToAction`, `Slider`, `Content`, `Gallery`, or others.
|
||||
- A form builder tool where available block configs might be `Text`, `Select`, or `Checkbox`.
|
||||
- Virtual event agenda "timeslots" where a timeslot could either be a `Break`, a `Presentation`, or a `BreakoutSession`.
|
||||
|
||||

|
||||
_Admin panel screenshot of a Blocks field type with Call to Action and Number block examples_
|
||||
|
||||
### Field config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: checkbox, fields, config, configuration, documentation, Content Manage
|
||||
The Checkbox field type saves a boolean in the database.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/checkbox.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/checkbox-dark.png'
|
||||
alt='Checkbox field with text field in Payload admin panel'
|
||||
caption='Admin panel screenshot of Checkbox field with Text field below'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -11,6 +11,13 @@ keywords: code, fields, config, configuration, documentation, Content Management
|
||||
The Code field type saves a string in the database, but provides the Admin panel with a code editor styled interface.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/code.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/code-dark.png'
|
||||
alt='Shows a Code field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Code field'
|
||||
/>
|
||||
|
||||
This field uses the `monaco-react` editor syntax highlighting.
|
||||
|
||||
### Config
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: row, fields, config, configuration, documentation, Content Management
|
||||
The Collapsible field is presentational-only and only affects the Admin panel. By using it, you can place fields within a nice layout component that can be collapsed / expanded.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/collapsible.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/collapsible-dark.png'
|
||||
alt='Shows a Collapsible field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Collapsible field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -11,6 +11,13 @@ keywords: date, fields, config, configuration, documentation, Content Management
|
||||
with a customizable time picker interface.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/date.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/date-dark.png'
|
||||
alt='Shows a Date field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Date field'
|
||||
/>
|
||||
|
||||
This field uses [`react-datepicker`](https://www.npmjs.com/package/react-datepicker) for the Admin panel component.
|
||||
|
||||
### Config
|
||||
|
||||
@@ -6,10 +6,17 @@ desc: The Email field enforces that the value provided is a valid email address.
|
||||
keywords: email, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
<Banner >
|
||||
<Banner>
|
||||
The Email field enforces that the value provided is a valid email address.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/email.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/email-dark.png'
|
||||
alt='Shows an Email field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of an Email field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -11,6 +11,13 @@ keywords: group, fields, config, configuration, documentation, Content Managemen
|
||||
also groups fields together visually in the Admin panel.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/group.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/group-dark.png'
|
||||
alt='Shows a Group field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Group field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -11,6 +11,13 @@ keywords: json, fields, config, configuration, documentation, Content Management
|
||||
The JSON field type saves actual JSON in the database, which differs from the Code field that saves the value as a string in the database.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/json.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/json-dark.png'
|
||||
alt='Shows a JSON field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a JSON field'
|
||||
/>
|
||||
|
||||
This field uses the `monaco-react` editor syntax highlighting.
|
||||
|
||||
### Config
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: number, fields, config, configuration, documentation, Content Manageme
|
||||
The Number field stores and validates numeric entry and supports additional numerical validation and formatting features.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/number.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/number-dark.png'
|
||||
alt='Shows a Number field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Number field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -11,6 +11,13 @@ keywords: point, geolocation, geospatial, geojson, 2dsphere, config, configurati
|
||||
The Point field type saves a pair of coordinates in the database and assigns an index for location related queries.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/point.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/point-dark.png'
|
||||
alt='Shows a Point field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Point field'
|
||||
/>
|
||||
|
||||
The data structure in the database matches the GeoJSON structure to represent point. The Payload APIs simplifies the object data to only the [longitude, latitude] location.
|
||||
|
||||
### Config
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: radio, fields, config, configuration, documentation, Content Managemen
|
||||
The Radio Group field type allows for the selection of one value from a predefined set of possible values and presents a radio group-style set of inputs to the Admin panel.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/radio.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/radio-dark.png'
|
||||
alt='Shows a Radio field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Radio field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -11,6 +11,13 @@ keywords: relationship, fields, config, configuration, documentation, Content Ma
|
||||
provides for the ability to easily relate documents together.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/relationship.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/relationship-dark.png'
|
||||
alt='Shows a relationship field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Relationship field'
|
||||
/>
|
||||
|
||||
**Example uses:**
|
||||
|
||||
- To add `Product` documents to an `Order` document
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: rich text, fields, config, configuration, documentation, Content Manag
|
||||
The Rich Text field is a powerful way to allow editors to write dynamic content. The content is saved as JSON in the database and can be converted into any format, including HTML, that you need.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/richtext.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/richtext-dark.png'
|
||||
alt='Shows a Rich Text field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Rich Text field'
|
||||
/>
|
||||
|
||||
The Admin component is built on the powerful [`slatejs`](https://docs.slatejs.org/) editor and is meant to be as extensible and customizable as possible.
|
||||
|
||||
<Banner type="success">
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: row, fields, config, configuration, documentation, Content Management
|
||||
The Row field is presentational-only and only affects the Admin panel. By using it, you can arrange fields next to each other horizontally.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/row.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/row-dark.png'
|
||||
alt='Shows a row field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Row field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -11,6 +11,13 @@ keywords: select, multi-select, fields, config, configuration, documentation, Co
|
||||
a predefined list as an enumeration.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/select.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/select-dark.png'
|
||||
alt='Shows a Select field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Select field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -12,8 +12,13 @@ keywords: tabs, fields, config, configuration, documentation, Content Management
|
||||
component that separates certain sub-fields by a tabbed interface.
|
||||
</Banner>
|
||||
|
||||

|
||||
_Tabs field type used to separate Hero fields from Page Layout_
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/tabs.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/tabs-dark.png'
|
||||
alt='Shows a tabs field used to separate Hero and Page layout in the Payload admin panel'
|
||||
caption='Tabs field type used to separate Hero fields from Page Layout'
|
||||
/>
|
||||
|
||||
|
||||
### Config
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: text, fields, config, configuration, documentation, Content Management
|
||||
The Text field type is one of the most commonly used fields. It saves a string to the database and provides the Admin panel with a simple text input.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/text.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/text-dark.png'
|
||||
alt='Shows a text field and read-only text field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Text field and read-only Text field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -10,6 +10,13 @@ keywords: textarea, fields, config, configuration, documentation, Content Manage
|
||||
The Textarea field is almost identical to the Text field but it features a slightly larger input that is better suited to edit longer text.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/textarea.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/textarea-dark.png'
|
||||
alt='Shows a textarea field and read-only textarea field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of a Textarea field and read-only Textarea field'
|
||||
/>
|
||||
|
||||
### Config
|
||||
|
||||
| Option | Description |
|
||||
|
||||
@@ -15,6 +15,13 @@ keywords: upload, images media, fields, config, configuration, documentation, Co
|
||||
To use this field, you need to have a Collection configured to allow Uploads. For more information, [click here](/docs/upload/overview) to read about how to enable Uploads on a collection by collection basis.
|
||||
</Banner>
|
||||
|
||||
<LightDarkImage
|
||||
srcLight='https://payloadcms.com/images/docs/fields/upload.png'
|
||||
srcDark='https://payloadcms.com/images/docs/fields/upload-dark.png'
|
||||
alt='Shows an upload field in the Payload admin panel'
|
||||
caption='Admin panel screenshot of an Upload field'
|
||||
/>
|
||||
|
||||
**Example uses:**
|
||||
|
||||
- To provide a `Page` with a featured image
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { FormFieldBlock } from "payload-plugin-form-builder/dist/types"
|
||||
import { FormFieldBlock } from 'payload-plugin-form-builder/dist/types'
|
||||
|
||||
export const buildInitialFormState = (fields: FormFieldBlock[]) => {
|
||||
return fields.reduce((initialSchema, field) => {
|
||||
if (field.blockType === 'checkbox') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.blockName]: false,
|
||||
[field.name]: false,
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'country') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.blockName]: '',
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'email') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.blockName]: '',
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'text') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.blockName]: '',
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'select') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.blockName]: '',
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
if (field.blockType === 'state') {
|
||||
return {
|
||||
...initialSchema,
|
||||
[field.blockName]: '',
|
||||
[field.name]: '',
|
||||
}
|
||||
}
|
||||
}, {})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "1.12.0",
|
||||
"version": "1.13.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@import '../../../scss/styles.scss';
|
||||
|
||||
.thumbnail-card {
|
||||
@include btn-reset;
|
||||
@include shadow;
|
||||
width: 100%;
|
||||
background: var(--theme-input-bg);
|
||||
|
||||
&__label {
|
||||
|
||||
@@ -18,7 +18,6 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
|
||||
thumbnail,
|
||||
label: labelFromProps,
|
||||
alignLabel,
|
||||
onKeyDown,
|
||||
} = props;
|
||||
|
||||
const { t, i18n } = useTranslation('general');
|
||||
@@ -43,11 +42,11 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
title={title}
|
||||
className={classes}
|
||||
onClick={typeof onClick === 'function' ? onClick : undefined}
|
||||
onKeyDown={typeof onKeyDown === 'function' ? onKeyDown : undefined}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={`${baseClass}__thumbnail`}>
|
||||
{thumbnail && thumbnail}
|
||||
@@ -62,6 +61,6 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__label`}>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactSelect from '../../../ReactSelect';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
import { Props } from './types';
|
||||
import { Option, OptionObject } from '../../../../../../fields/config/types';
|
||||
|
||||
const formatOptions = (options: Option[]): OptionObject[] => options.map((option) => {
|
||||
if (typeof option === 'object' && (option.value || option.value === '')) {
|
||||
return option;
|
||||
}
|
||||
|
||||
return {
|
||||
label: option,
|
||||
value: option,
|
||||
} as OptionObject;
|
||||
});
|
||||
|
||||
export const Select: React.FC<Props> = ({ onChange, value, options: optionsFromProps, operator }) => {
|
||||
const { i18n } = useTranslation();
|
||||
const [options, setOptions] = React.useState(formatOptions(optionsFromProps));
|
||||
|
||||
const isMulti = ['in', 'not_in'].includes(operator);
|
||||
let valueToRender;
|
||||
|
||||
if (isMulti && Array.isArray(value)) {
|
||||
valueToRender = value.map((val) => {
|
||||
const matchingOption = options.find((option) => option.value === val);
|
||||
return {
|
||||
label: matchingOption ? getTranslation(matchingOption.label, i18n) : val,
|
||||
value: matchingOption?.value ?? val,
|
||||
};
|
||||
});
|
||||
} else if (value) {
|
||||
const matchingOption = options.find((option) => option.value === value);
|
||||
valueToRender = {
|
||||
label: matchingOption ? getTranslation(matchingOption.label, i18n) : value,
|
||||
value: matchingOption?.value ?? value,
|
||||
};
|
||||
}
|
||||
|
||||
const onSelect = React.useCallback((selectedOption) => {
|
||||
let newValue;
|
||||
if (!selectedOption) {
|
||||
newValue = null;
|
||||
} else if (isMulti) {
|
||||
if (Array.isArray(selectedOption)) {
|
||||
newValue = selectedOption.map((option) => option.value);
|
||||
} else {
|
||||
newValue = [];
|
||||
}
|
||||
} else {
|
||||
newValue = selectedOption.value;
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
}, [
|
||||
isMulti,
|
||||
onChange,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setOptions(formatOptions(optionsFromProps));
|
||||
}, [optionsFromProps]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isMulti && Array.isArray(value)) {
|
||||
onChange(value[0]);
|
||||
}
|
||||
}, [isMulti, onChange, value]);
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
onChange={onSelect}
|
||||
value={valueToRender}
|
||||
options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}
|
||||
isMulti={isMulti}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Operator } from '../../../../../../types';
|
||||
import { Option } from '../../../../../../fields/config/types';
|
||||
|
||||
export type Props = {
|
||||
onChange: (val: string) => void,
|
||||
value: string,
|
||||
options: Option[]
|
||||
operator: Operator
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import Date from './Date';
|
||||
import Number from './Number';
|
||||
import Text from './Text';
|
||||
import Relationship from './Relationship';
|
||||
import { Select } from './Select';
|
||||
import useDebounce from '../../../../hooks/useDebounce';
|
||||
import { FieldCondition } from '../types';
|
||||
|
||||
@@ -17,6 +18,7 @@ const valueFields = {
|
||||
Number,
|
||||
Text,
|
||||
Relationship,
|
||||
Select,
|
||||
};
|
||||
|
||||
const baseClass = 'condition';
|
||||
@@ -56,7 +58,15 @@ const Condition: React.FC<Props> = (props) => {
|
||||
});
|
||||
}, [debouncedValue, dispatch, orIndex, andIndex]);
|
||||
|
||||
const ValueComponent = valueFields[activeField?.component] || valueFields.Text;
|
||||
const booleanSelect = ['exists'].includes(operatorValue) || activeField.props.type === 'checkbox';
|
||||
const ValueComponent = booleanSelect ? Select : (valueFields[activeField?.component] || valueFields.Text);
|
||||
|
||||
let selectOptions;
|
||||
if (booleanSelect) {
|
||||
selectOptions = ['true', 'false'];
|
||||
} else if ('options' in activeField?.props) {
|
||||
selectOptions = activeField.props.options;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -95,6 +105,7 @@ const Condition: React.FC<Props> = (props) => {
|
||||
DefaultComponent={ValueComponent}
|
||||
componentProps={{
|
||||
...activeField?.props,
|
||||
options: selectOptions,
|
||||
operator: operatorValue,
|
||||
value: internalValue,
|
||||
onChange: setInternalValue,
|
||||
|
||||
@@ -112,8 +112,12 @@ const fieldTypeConditions = {
|
||||
component: 'Relationship',
|
||||
operators: [...base],
|
||||
},
|
||||
radio: {
|
||||
component: 'Select',
|
||||
operators: [...base],
|
||||
},
|
||||
select: {
|
||||
component: 'Text',
|
||||
component: 'Select',
|
||||
operators: [...base],
|
||||
},
|
||||
checkbox: {
|
||||
|
||||
@@ -61,7 +61,6 @@ const Form: React.FC<Props> = (props) => {
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [formattedInitialData, setFormattedInitialData] = useState(buildInitialState(initialData));
|
||||
const [collectionFieldSchemaMap, setCollectionFieldSchemaMap] = useState(new Map<string, Field[]>());
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const contextRef = useRef({} as FormContextType);
|
||||
@@ -362,18 +361,74 @@ const Form: React.FC<Props> = (props) => {
|
||||
waitForAutocomplete,
|
||||
]);
|
||||
|
||||
const traverseRowConfigs = React.useCallback(({ pathPrefix, path, fieldConfig }: {
|
||||
path: string,
|
||||
fieldConfig: Field[]
|
||||
pathPrefix?: string,
|
||||
}) => {
|
||||
const config = fieldConfig;
|
||||
const pathSegments = splitPathByArrayFields(path);
|
||||
const configMap = buildFieldSchemaMap(config);
|
||||
|
||||
for (let i = 0; i < pathSegments.length; i += 1) {
|
||||
const pathSegment = pathSegments[i];
|
||||
|
||||
if (isNumber(pathSegment)) {
|
||||
const rowIndex = parseInt(pathSegment, 10);
|
||||
const parentFieldPath = pathSegments.slice(0, i).join('.');
|
||||
const remainingPath = pathSegments.slice(i + 1).join('.');
|
||||
const arrayFieldPath = pathPrefix ? `${pathPrefix}.${parentFieldPath}` : parentFieldPath;
|
||||
const parentArrayField = contextRef.current.getField(arrayFieldPath);
|
||||
const rowField = parentArrayField.rows[rowIndex];
|
||||
|
||||
if (rowField.blockType) {
|
||||
const blockConfig = configMap.get(`${parentFieldPath}.${rowField.blockType}`);
|
||||
if (blockConfig) {
|
||||
return traverseRowConfigs({
|
||||
pathPrefix: `${arrayFieldPath}.${rowIndex}`,
|
||||
path: remainingPath,
|
||||
fieldConfig: blockConfig,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Block config not found for ${rowField.blockType} at path ${path}`);
|
||||
} else {
|
||||
return traverseRowConfigs({
|
||||
pathPrefix: `${arrayFieldPath}.${rowIndex}`,
|
||||
path: remainingPath,
|
||||
fieldConfig: configMap.get(parentFieldPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}, []);
|
||||
|
||||
const getRowConfigByPath = React.useCallback(({ path, blockType }: {
|
||||
path: string,
|
||||
blockType?: string
|
||||
}) => {
|
||||
const rowConfig = traverseRowConfigs({ path, fieldConfig: collection?.fields || global?.fields });
|
||||
const rowFieldConfigs = buildFieldSchemaMap(rowConfig);
|
||||
const pathSegments = splitPathByArrayFields(path);
|
||||
const fieldKey = pathSegments.at(-1);
|
||||
return rowFieldConfigs.get(blockType ? `${fieldKey}.${blockType}` : fieldKey);
|
||||
}, [traverseRowConfigs, collection?.fields, global?.fields]);
|
||||
|
||||
// Array/Block row manipulation
|
||||
const addFieldRow: Context['addFieldRow'] = useCallback(async ({ path, rowIndex, data }) => {
|
||||
const preferences = await getDocPreferences();
|
||||
const nonIndexedPath = path.split('.').filter((segment) => !isNumber(segment)).join('.');
|
||||
const schemaKey = data?.blockType ? `${nonIndexedPath}.${data.blockType}` : nonIndexedPath;
|
||||
const rowFieldSchema = collectionFieldSchemaMap.get(schemaKey);
|
||||
const fieldConfig = getRowConfigByPath({
|
||||
path,
|
||||
blockType: data?.blockType,
|
||||
});
|
||||
|
||||
if (rowFieldSchema) {
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: rowFieldSchema, data, preferences, operation, id, user, locale, t });
|
||||
if (fieldConfig) {
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: fieldConfig, data, preferences, operation, id, user, locale, t });
|
||||
dispatchFields({ type: 'ADD_ROW', rowIndex, path, blockType: data?.blockType, subFieldState });
|
||||
}
|
||||
}, [dispatchFields, collectionFieldSchemaMap, getDocPreferences, id, user, operation, locale, t]);
|
||||
}, [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath]);
|
||||
|
||||
const removeFieldRow: Context['removeFieldRow'] = useCallback(async ({ path, rowIndex }) => {
|
||||
dispatchFields({ type: 'REMOVE_ROW', rowIndex, path });
|
||||
@@ -381,15 +436,16 @@ const Form: React.FC<Props> = (props) => {
|
||||
|
||||
const replaceFieldRow: Context['replaceFieldRow'] = useCallback(async ({ path, rowIndex, data }) => {
|
||||
const preferences = await getDocPreferences();
|
||||
const nonIndexedPath = path.split('.').filter((segment) => !isNumber(segment)).join('.');
|
||||
const schemaKey = data?.blockType ? `${nonIndexedPath}.${data.blockType}` : nonIndexedPath;
|
||||
const rowFieldSchema = collectionFieldSchemaMap.get(schemaKey);
|
||||
const fieldConfig = getRowConfigByPath({
|
||||
path,
|
||||
blockType: data?.blockType,
|
||||
});
|
||||
|
||||
if (rowFieldSchema) {
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: rowFieldSchema, data, preferences, operation, id, user, locale, t });
|
||||
if (fieldConfig) {
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: fieldConfig, data, preferences, operation, id, user, locale, t });
|
||||
dispatchFields({ type: 'REPLACE_ROW', rowIndex, path, blockType: data?.blockType, subFieldState });
|
||||
}
|
||||
}, [dispatchFields, collectionFieldSchemaMap, getDocPreferences, id, user, operation, locale, t]);
|
||||
}, [dispatchFields, getDocPreferences, id, user, operation, locale, t, getRowConfigByPath]);
|
||||
|
||||
const getFields = useCallback(() => contextRef.current.fields, [contextRef]);
|
||||
const getField = useCallback((path: string) => contextRef.current.fields[path], [contextRef]);
|
||||
@@ -455,13 +511,6 @@ const Form: React.FC<Props> = (props) => {
|
||||
contextRef.current.removeFieldRow = removeFieldRow;
|
||||
contextRef.current.replaceFieldRow = replaceFieldRow;
|
||||
|
||||
useEffect(() => {
|
||||
const entityFields = collection?.fields || global?.fields || [];
|
||||
if (entityFields.length === 0) return;
|
||||
const fieldSchemaMap = buildFieldSchemaMap(entityFields);
|
||||
setCollectionFieldSchemaMap(fieldSchemaMap);
|
||||
}, [collection?.fields, global?.fields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialState) {
|
||||
contextRef.current = { ...initContextState } as FormContextType;
|
||||
|
||||
@@ -2,22 +2,16 @@
|
||||
|
||||
.blocks-drawer {
|
||||
&__blocks-wrapper {
|
||||
padding: base(0.5);
|
||||
margin-top: base(1.5);
|
||||
padding-top: base(1.5);
|
||||
}
|
||||
|
||||
&__blocks {
|
||||
position: relative;
|
||||
margin: -#{base(1)};
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&__block {
|
||||
margin: base(0.5);
|
||||
width: calc((100% / 6) - #{base(1)});
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: base(1);
|
||||
}
|
||||
|
||||
&__default-image {
|
||||
@@ -27,33 +21,28 @@
|
||||
}
|
||||
|
||||
@include large-break {
|
||||
&__block {
|
||||
width: calc(20% - #{base(1)});
|
||||
&__blocks {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@include mid-break {
|
||||
&__blocks-wrapper {
|
||||
padding: base(0.25);
|
||||
padding-top: base(1.75);
|
||||
}
|
||||
|
||||
&__blocks {
|
||||
margin: -#{base(0.5)};
|
||||
}
|
||||
|
||||
&__block {
|
||||
margin: base(0.25);
|
||||
width: calc(33.33% - #{base(0.5)});
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: base(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@include small-break {
|
||||
&__blocks-wrapper {
|
||||
margin-top: base(0.75);
|
||||
padding-top: base(.75);
|
||||
}
|
||||
|
||||
&__block {
|
||||
width: calc(50% - #{base(0.5)});
|
||||
&__blocks {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { requests } from '../../../api';
|
||||
|
||||
type PreferencesContext = {
|
||||
getPreference: <T = any>(key: string) => T | Promise<T>;
|
||||
setPreference: <T = any>(key: string, value: T) => void;
|
||||
setPreference: <T = any>(key: string, value: T) => Promise<void>;
|
||||
}
|
||||
|
||||
const Context = createContext({} as PreferencesContext);
|
||||
|
||||
@@ -21,6 +21,11 @@ const useThumbnail = (collection: SanitizedCollectionConfig, doc: Record<string,
|
||||
} = doc;
|
||||
|
||||
const { serverURL } = useConfig();
|
||||
let pathURL = `${serverURL}${staticURL || ''}`;
|
||||
|
||||
if (absoluteURLPattern.test(staticURL)) {
|
||||
pathURL = staticURL;
|
||||
}
|
||||
|
||||
if (typeof adminThumbnail === 'function') {
|
||||
const thumbnailURL = adminThumbnail({ doc });
|
||||
@@ -31,7 +36,7 @@ const useThumbnail = (collection: SanitizedCollectionConfig, doc: Record<string,
|
||||
return thumbnailURL;
|
||||
}
|
||||
|
||||
return `${serverURL}${thumbnailURL}`;
|
||||
return `${pathURL}/${thumbnailURL}`;
|
||||
}
|
||||
|
||||
if (isImage(mimeType as string)) {
|
||||
@@ -44,10 +49,10 @@ const useThumbnail = (collection: SanitizedCollectionConfig, doc: Record<string,
|
||||
}
|
||||
|
||||
if (sizes?.[adminThumbnail]?.filename) {
|
||||
return `${serverURL}${staticURL}/${sizes[adminThumbnail].filename}`;
|
||||
return `${pathURL}/${sizes[adminThumbnail].filename}`;
|
||||
}
|
||||
|
||||
return `${serverURL}${staticURL}/${filename}`;
|
||||
return `${pathURL}/${filename}`;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -6,3 +6,12 @@
|
||||
padding: 0;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
@mixin btn-reset {
|
||||
border: 0;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,99 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { User } from '..';
|
||||
import { CollectionConfig } from '../../collections/config/types';
|
||||
import { Field, fieldAffectsData, fieldHasSubFields } from '../../fields/config/types';
|
||||
import { Field, fieldAffectsData, TabAsField, tabHasName } from '../../fields/config/types';
|
||||
|
||||
type TraverseFieldsArgs = {
|
||||
fields: (Field | TabAsField)[]
|
||||
data: Record<string, unknown>
|
||||
result: Record<string, unknown>
|
||||
}
|
||||
const traverseFields = ({
|
||||
// parent,
|
||||
fields,
|
||||
data,
|
||||
result,
|
||||
}: TraverseFieldsArgs) => {
|
||||
fields.forEach((field) => {
|
||||
switch (field.type) {
|
||||
case 'row':
|
||||
case 'collapsible': {
|
||||
traverseFields({
|
||||
fields: field.fields,
|
||||
data,
|
||||
result,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'group': {
|
||||
let targetResult;
|
||||
if (typeof field.saveToJWT === 'string') {
|
||||
targetResult = field.saveToJWT;
|
||||
result[field.saveToJWT] = data[field.name];
|
||||
} else if (field.saveToJWT) {
|
||||
targetResult = field.name;
|
||||
result[field.name] = data[field.name];
|
||||
}
|
||||
const groupData: Record<string, unknown> = data[field.name] as Record<string, unknown>;
|
||||
const groupResult = (targetResult ? result[targetResult] : result) as Record<string, unknown>;
|
||||
traverseFields({
|
||||
fields: field.fields,
|
||||
data: groupData,
|
||||
result: groupResult,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'tabs': {
|
||||
traverseFields({
|
||||
fields: field.tabs.map((tab) => ({ ...tab, type: 'tab' })),
|
||||
data,
|
||||
result,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'tab': {
|
||||
if (tabHasName(field)) {
|
||||
let targetResult;
|
||||
if (typeof field.saveToJWT === 'string') {
|
||||
targetResult = field.saveToJWT;
|
||||
result[field.saveToJWT] = data[field.name];
|
||||
} else if (field.saveToJWT) {
|
||||
targetResult = field.name;
|
||||
result[field.name] = data[field.name];
|
||||
}
|
||||
const tabData: Record<string, unknown> = data[field.name] as Record<string, unknown>;
|
||||
const tabResult = (targetResult ? result[targetResult] : result) as Record<string, unknown>;
|
||||
traverseFields({
|
||||
fields: field.fields,
|
||||
data: tabData,
|
||||
result: tabResult,
|
||||
});
|
||||
} else {
|
||||
traverseFields({
|
||||
fields: field.fields,
|
||||
data,
|
||||
result,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (fieldAffectsData(field)) {
|
||||
if (field.saveToJWT) {
|
||||
if (typeof field.saveToJWT === 'string') {
|
||||
result[field.saveToJWT] = data[field.name];
|
||||
delete result[field.name];
|
||||
} else {
|
||||
result[field.name] = data[field.name] as Record<string, unknown>;
|
||||
}
|
||||
} else if (field.saveToJWT === false) {
|
||||
delete result[field.name];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
export const getFieldsToSign = (args: {
|
||||
collectionConfig: CollectionConfig,
|
||||
user: User
|
||||
@@ -13,28 +105,17 @@ export const getFieldsToSign = (args: {
|
||||
email,
|
||||
} = args;
|
||||
|
||||
return collectionConfig.fields.reduce((signedFields, field: Field) => {
|
||||
const result = {
|
||||
...signedFields,
|
||||
};
|
||||
|
||||
// get subfields from non-named fields like rows
|
||||
if (!fieldAffectsData(field) && fieldHasSubFields(field)) {
|
||||
field.fields.forEach((subField) => {
|
||||
if (fieldAffectsData(subField) && subField.saveToJWT) {
|
||||
result[typeof subField.saveToJWT === 'string' ? subField.saveToJWT : subField.name] = user[subField.name];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fieldAffectsData(field) && field.saveToJWT) {
|
||||
result[typeof field.saveToJWT === 'string' ? field.saveToJWT : field.name] = user[field.name];
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {
|
||||
const result: Record<string, unknown> = {
|
||||
email,
|
||||
id: user.id,
|
||||
collection: collectionConfig.slug,
|
||||
};
|
||||
|
||||
traverseFields({
|
||||
fields: collectionConfig.fields,
|
||||
data: user,
|
||||
result,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -228,6 +228,10 @@ const tab = baseField.keys({
|
||||
.when('localized', { is: joi.exist(), then: joi.required() }),
|
||||
localized: joi.boolean(),
|
||||
interfaceName: joi.string().when('name', { not: joi.exist(), then: joi.forbidden() }),
|
||||
saveToJWT: joi.alternatives().try(
|
||||
joi.boolean(),
|
||||
joi.string(),
|
||||
),
|
||||
label: joi.alternatives().try(
|
||||
joi.string(),
|
||||
joi.object().pattern(joi.string(), [joi.string()]),
|
||||
|
||||
@@ -234,6 +234,7 @@ export type TabsAdmin = Omit<Admin, 'description'>;
|
||||
|
||||
type TabBase = Omit<FieldBase, 'required' | 'validation'> & {
|
||||
fields: Field[]
|
||||
saveToJWT?: boolean | string
|
||||
description?: Description
|
||||
interfaceName?: string
|
||||
}
|
||||
@@ -256,7 +257,7 @@ export type UnnamedTab = Omit<TabBase, 'name'> & {
|
||||
|
||||
export type Tab = NamedTab | UnnamedTab
|
||||
|
||||
export type TabsField = Omit<FieldBase, 'admin' | 'name' | 'localized'> & {
|
||||
export type TabsField = Omit<FieldBase, 'admin' | 'name' | 'localized' | 'saveToJWT'> & {
|
||||
type: 'tabs';
|
||||
tabs: Tab[]
|
||||
admin?: TabsAdmin
|
||||
|
||||
@@ -47,7 +47,11 @@ export const buildFieldSchemaMap = (entityFields: Field[]): Map<string, Field[]>
|
||||
|
||||
case 'tabs':
|
||||
field.tabs.forEach((tab) => {
|
||||
nextPath = 'name' in tab ? `${nextPath}.${tab.name}` : nextPath;
|
||||
if (nextPath) {
|
||||
nextPath = 'name' in tab ? `${nextPath}.${tab.name}` : nextPath;
|
||||
} else {
|
||||
nextPath = 'name' in tab ? `${tab.name}` : nextPath;
|
||||
}
|
||||
buildUpMap(tab.fields, nextPath);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -52,6 +52,88 @@ export default buildConfigWithDefaults({
|
||||
defaultValue: namedSaveToJWTValue,
|
||||
saveToJWT: saveToJWTKey,
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'liftedSaveToJWT',
|
||||
type: 'text',
|
||||
saveToJWT: 'x-lifted-from-group',
|
||||
defaultValue: 'lifted from group',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupSaveToJWT',
|
||||
type: 'group',
|
||||
saveToJWT: 'x-group',
|
||||
fields: [
|
||||
{
|
||||
name: 'saveToJWTString',
|
||||
type: 'text',
|
||||
saveToJWT: 'x-test',
|
||||
defaultValue: 'nested property',
|
||||
},
|
||||
{
|
||||
name: 'saveToJWTFalse',
|
||||
type: 'text',
|
||||
saveToJWT: false,
|
||||
defaultValue: 'nested property',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
name: 'saveToJWTTab',
|
||||
saveToJWT: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'test',
|
||||
type: 'text',
|
||||
saveToJWT: 'x-field',
|
||||
defaultValue: 'yes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'tabSaveToJWTString',
|
||||
saveToJWT: 'tab-test',
|
||||
fields: [
|
||||
{
|
||||
name: 'includedByDefault',
|
||||
type: 'text',
|
||||
defaultValue: 'yes',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'No Name',
|
||||
fields: [
|
||||
{
|
||||
name: 'tabLiftedSaveToJWT',
|
||||
type: 'text',
|
||||
saveToJWT: true,
|
||||
defaultValue: 'lifted from unnamed tab',
|
||||
},
|
||||
{
|
||||
name: 'unnamedTabSaveToJWTString',
|
||||
type: 'text',
|
||||
saveToJWT: 'x-tab-field',
|
||||
defaultValue: 'text',
|
||||
},
|
||||
{
|
||||
name: 'unnamedTabSaveToJWTFalse',
|
||||
type: 'text',
|
||||
saveToJWT: false,
|
||||
defaultValue: 'false',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'custom',
|
||||
label: 'Custom',
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import mongoose from 'mongoose';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { GraphQLClient } from 'graphql-request';
|
||||
import payload from '../../src';
|
||||
import { initPayloadTest } from '../helpers/configHelpers';
|
||||
import { namedSaveToJWTValue, saveToJWTKey, slug } from './config';
|
||||
import { devUser } from '../credentials';
|
||||
import type { User } from '../../src/auth';
|
||||
import configPromise from '../collections-graphql/config';
|
||||
|
||||
require('isomorphic-fetch');
|
||||
|
||||
let apiUrl;
|
||||
let client: GraphQLClient;
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -20,6 +23,9 @@ describe('Auth', () => {
|
||||
beforeAll(async () => {
|
||||
const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } });
|
||||
apiUrl = `${serverURL}/api`;
|
||||
const config = await configPromise;
|
||||
const url = `${serverURL}${config.routes.api}${config.routes.graphQL}`;
|
||||
client = new GraphQLClient(url);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -28,7 +34,50 @@ describe('Auth', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('admin user', () => {
|
||||
describe('GraphQL - admin user', () => {
|
||||
let token;
|
||||
let user;
|
||||
beforeAll(async () => {
|
||||
// language=graphQL
|
||||
const query = `mutation {
|
||||
loginUser(email: "${devUser.email}", password: "${devUser.password}") {
|
||||
token
|
||||
user {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
}`;
|
||||
const response = await client.request(query);
|
||||
user = response.loginUser.user;
|
||||
token = response.loginUser.token;
|
||||
});
|
||||
|
||||
it('should login', async () => {
|
||||
expect(user.id).toBeDefined();
|
||||
expect(user.email).toEqual(devUser.email);
|
||||
expect(token).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have fields saved to JWT', async () => {
|
||||
const decoded = jwtDecode<User>(token);
|
||||
const {
|
||||
email: jwtEmail,
|
||||
collection,
|
||||
roles,
|
||||
iat,
|
||||
exp,
|
||||
} = decoded;
|
||||
|
||||
expect(jwtEmail).toBeDefined();
|
||||
expect(collection).toEqual('users');
|
||||
expect(Array.isArray(roles)).toBeTruthy();
|
||||
expect(iat).toBeDefined();
|
||||
expect(exp).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('REST - admin user', () => {
|
||||
beforeAll(async () => {
|
||||
await fetch(`${apiUrl}/${slug}/first-register`, {
|
||||
body: JSON.stringify({
|
||||
@@ -103,20 +152,39 @@ describe('Auth', () => {
|
||||
});
|
||||
|
||||
it('should have fields saved to JWT', async () => {
|
||||
const decoded = jwtDecode<User>(token);
|
||||
const {
|
||||
email: jwtEmail,
|
||||
collection,
|
||||
roles,
|
||||
[saveToJWTKey]: customJWTPropertyKey,
|
||||
'x-lifted-from-group': liftedFromGroup,
|
||||
'x-tab-field': unnamedTabSaveToJWTString,
|
||||
tabLiftedSaveToJWT,
|
||||
unnamedTabSaveToJWTFalse,
|
||||
iat,
|
||||
exp,
|
||||
} = jwtDecode<User>(token);
|
||||
} = decoded;
|
||||
|
||||
const group = decoded['x-group'] as Record<string, unknown>;
|
||||
const tab = decoded.saveToJWTTab as Record<string, unknown>;
|
||||
const tabString = decoded['tab-test'] as Record<string, unknown>;
|
||||
|
||||
expect(jwtEmail).toBeDefined();
|
||||
expect(collection).toEqual('users');
|
||||
expect(collection).toEqual('users');
|
||||
expect(Array.isArray(roles)).toBeTruthy();
|
||||
// 'x-custom-jwt-property-name': 'namedSaveToJWT value'
|
||||
expect(customJWTPropertyKey).toEqual(namedSaveToJWTValue);
|
||||
expect(group).toBeDefined();
|
||||
expect(group['x-test']).toEqual('nested property');
|
||||
expect(group.saveToJWTFalse).toBeUndefined();
|
||||
expect(liftedFromGroup).toEqual('lifted from group');
|
||||
expect(tabLiftedSaveToJWT).toEqual('lifted from unnamed tab');
|
||||
expect(tab['x-field']).toEqual('yes');
|
||||
expect(tabString.includedByDefault).toEqual('yes');
|
||||
expect(unnamedTabSaveToJWTString).toEqual('text');
|
||||
expect(unnamedTabSaveToJWTFalse).toBeUndefined();
|
||||
expect(iat).toBeDefined();
|
||||
expect(exp).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -203,6 +203,42 @@ const BlockFields: CollectionConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'blocksWithSimilarConfigs',
|
||||
blocks: [{
|
||||
slug: 'block-1',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'items',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'block-2',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'items',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title2',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -368,6 +368,29 @@ describe('fields', () => {
|
||||
await expect(firstRow).toBeVisible();
|
||||
await expect(firstRow.locator('.blocks-field__block-pill-text')).toContainText('Text en');
|
||||
});
|
||||
|
||||
test('should add different blocks with similar field configs', async () => {
|
||||
await page.goto(url.create);
|
||||
|
||||
async function addBlock(name: 'Block 1' | 'Block 2') {
|
||||
await page.locator('#field-blocksWithSimilarConfigs').getByRole('button', { name: 'Add Blocks With Similar Config' }).click();
|
||||
await page.getByRole('button', { name }).click();
|
||||
}
|
||||
|
||||
await addBlock('Block 1');
|
||||
|
||||
await page.locator('#blocksWithSimilarConfigs-row-0').getByRole('button', { name: 'Add Item' }).click();
|
||||
await page.locator('input[name="blocksWithSimilarConfigs.0.items.0.title"]').fill('items>0>title');
|
||||
|
||||
expect(await page.locator('input[name="blocksWithSimilarConfigs.0.items.0.title"]').inputValue()).toEqual('items>0>title');
|
||||
|
||||
await addBlock('Block 2');
|
||||
|
||||
await page.locator('#blocksWithSimilarConfigs-row-1').getByRole('button', { name: 'Add Item' }).click();
|
||||
await page.locator('input[name="blocksWithSimilarConfigs.1.items.0.title2"]').fill('items>1>title');
|
||||
|
||||
expect(await page.locator('input[name="blocksWithSimilarConfigs.1.items.0.title2"]').inputValue()).toEqual('items>1>title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('array', () => {
|
||||
|
||||
@@ -85,6 +85,89 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
type: 'tabs',
|
||||
label: 'Tabs',
|
||||
tabs: [{
|
||||
label: 'Tab 1',
|
||||
name: 'tab1',
|
||||
fields: [
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'layout',
|
||||
blocks: [{
|
||||
slug: 'block-1',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'items',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'block-2',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'items',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title2',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
{
|
||||
type: 'blocks',
|
||||
name: 'blocksWithSimilarConfigs',
|
||||
blocks: [{
|
||||
slug: 'block-1',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'items',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'block-2',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'items',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title2',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user