Merge branch 'master' of https://github.com/payloadcms/payload into feat/form-builder-example

This commit is contained in:
PatrikKozak
2023-03-15 11:58:48 -04:00
39 changed files with 537 additions and 132 deletions

View File

@@ -1,5 +1,37 @@
## [1.6.21](https://github.com/payloadcms/payload/compare/v1.6.20...v1.6.21) (2023-03-15)
### Bug Fixes
* hidden fields being mutated on patch ([#2317](https://github.com/payloadcms/payload/issues/2317)) ([8d65ba1](https://github.com/payloadcms/payload/commit/8d65ba1efd8744042bbaf669c10b6837a6b972f8))
## [1.6.20](https://github.com/payloadcms/payload/compare/v1.6.19...v1.6.20) (2023-03-13)
### Bug Fixes
* allow thumbnails in upload gallery to show useAsTitle value ([aae6d71](https://github.com/payloadcms/payload/commit/aae6d716e5608270ca142f2f4df214f9e271deb4))
* allows useListDrawer to work without collectionSlugs defined ([e1553c2](https://github.com/payloadcms/payload/commit/e1553c2fc88ac582744cd72d15c9e9ef3b8ec549))
* cancels existing fetches if new fetches are started ([ccc92fd](https://github.com/payloadcms/payload/commit/ccc92fdb7519e14ff1092f19ae4e7060fa413aab))
* check relationships indexed access for undefined ([959f017](https://github.com/payloadcms/payload/commit/959f01739c30450f3a6d052dd6083fdacf1527a4))
* ensures documentID exists in doc documentDrawers ([#2304](https://github.com/payloadcms/payload/issues/2304)) ([566c45b](https://github.com/payloadcms/payload/commit/566c45b0b436a9a3ea8eff27de2ea829dd6a2f0c))
* flattens title fields to allow seaching by title if title inside Row field ([75e776d](https://github.com/payloadcms/payload/commit/75e776ddb43b292eae6c1204589d9dc22deab50c))
* keep drop zone active when hovering inner elements ([#2295](https://github.com/payloadcms/payload/issues/2295)) ([39e303a](https://github.com/payloadcms/payload/commit/39e303add62d2dbd3e72d17e64e1ea5d940b0298))
* Prevent browser initial favicon request ([fd8ea88](https://github.com/payloadcms/payload/commit/fd8ea88488c80627346733e0595a2ef34c964a87))
* removes forced require on array, block, group ts ([657aa65](https://github.com/payloadcms/payload/commit/657aa65e993d13e9a294456b73adcd57f20d7c87))
* removes pagination type from top level admin config types ([bf9929e](https://github.com/payloadcms/payload/commit/bf9929e9a9919488f6de0e172909fa27719ecb04))
* renders presentational table columns ([4e1748f](https://github.com/payloadcms/payload/commit/4e1748fb8a3554586b377e60738130d03ec12f38))
* undefined point fields saving as empty object ([#2313](https://github.com/payloadcms/payload/issues/2313)) ([af16415](https://github.com/payloadcms/payload/commit/af164159fb52f4b0ef97e2fa34b881f97bc07310))
### Features
* [#2280](https://github.com/payloadcms/payload/issues/2280) Improve UX of paginator ([#2293](https://github.com/payloadcms/payload/issues/2293)) ([1df3d14](https://github.com/payloadcms/payload/commit/1df3d149e06cc955a61c4371371b601c0d9aad2b))
* exposes useTheme hook ([abebde6](https://github.com/payloadcms/payload/commit/abebde6b120a9dddc9971325b616b9cb31bcba90))
* provide refresh permissions for auth context ([e9c796e](https://github.com/payloadcms/payload/commit/e9c796e42c1bb1e0ce72d057ee88dee624b94c24))
## [1.6.19](https://github.com/payloadcms/payload/compare/v1.6.18...v1.6.19) (2023-03-09)

View File

@@ -4,3 +4,4 @@ exports.useDocumentInfo = require('../dist/admin/components/utilities/DocumentIn
exports.useConfig = require('../dist/admin/components/utilities/Config').useConfig;
exports.useAuth = require('../dist/admin/components/utilities/Auth').useAuth;
exports.useEditDepth = require('../dist/admin/components/utilities/EditDepth').useEditDepth;
exports.useTheme = require('../dist/admin/components/utilities/Theme').useTheme;

View File

@@ -226,14 +226,15 @@ const Greeting: React.FC = () => {
Useful to retrieve info about the currently logged in user as well as methods for interacting with it. It sends back an object with the following properties:
| Property | Description |
|---------------------|-----------------------------------------------------------------------------------------|
| **`user`** | The currently logged in user |
| **`logOut`** | A method to log out the currently logged in user |
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
| **`permissions`** | The permissions of the current user |
| Property | Description |
|--------------------------|-----------------------------------------------------------------------------------------|
| **`user`** | The currently logged in user |
| **`logOut`** | A method to log out the currently logged in user |
| **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token |
| **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory |
| **`token`** | The logged in user's token (useful for creating preview links, etc.) |
| **`refreshPermissions`** | Load new permissions (useful when content that effects permissions has been changed) |
| **`permissions`** | The permissions of the current user |
```tsx
import { useAuth } from 'payload/components/utilities';

View File

@@ -1,6 +1,6 @@
{
"name": "payload",
"version": "1.6.19",
"version": "1.6.21",
"description": "Node, React and MongoDB Headless CMS and Application Framework",
"license": "MIT",
"engines": {

View File

@@ -12,7 +12,7 @@ export const requests = {
}
return fetch(`${url}${query}`, {
credentials: 'include',
headers: options.headers,
...options,
});
},

View File

@@ -93,7 +93,10 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
if (isError) return null;
return (
<DocumentInfoProvider collection={collectionConfig}>
<DocumentInfoProvider
collection={collectionConfig}
id={id}
>
<RenderCustomComponent
DefaultComponent={DefaultEdit}
CustomComponent={collectionConfig.admin?.components?.views?.Edit}

View File

@@ -10,6 +10,7 @@ import Button from '../Button';
import { Props } from './types';
import { useSearchParams } from '../../utilities/SearchParams';
import validateWhereQuery from '../WhereBuilder/validateWhereQuery';
import flattenFields from '../../../../utilities/flattenTopLevelFields';
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched';
import { getTranslation } from '../../../../utilities/getTranslation';
@@ -37,7 +38,10 @@ const ListControls: React.FC<Props> = (props) => {
const params = useSearchParams();
const shouldInitializeWhereOpened = validateWhereQuery(params?.where);
const [titleField] = useState(() => fields.find((field) => fieldAffectsData(field) && field.name === useAsTitle));
const [titleField] = useState(() => {
const topLevelFields = flattenFields(fields);
return topLevelFields.find((field) => fieldAffectsData(field) && field.name === useAsTitle);
});
const [textFieldsToBeSearched] = useState(getTextFieldsToBeSearched(listSearchableFields, fields));
const [visibleDrawer, setVisibleDrawer] = useState<'where' | 'sort' | 'columns'>(shouldInitializeWhereOpened ? 'where' : undefined);
const { t, i18n } = useTranslation('general');

View File

@@ -4,6 +4,7 @@ import { ListDrawerProps, ListTogglerProps, UseListDrawer } from './types';
import { Drawer, DrawerToggler } from '../Drawer';
import { useEditDepth } from '../../utilities/EditDepth';
import { ListDrawerContent } from './DrawerContent';
import { useConfig } from '../../utilities/Config';
import './index.scss';
@@ -49,21 +50,26 @@ export const ListDrawer: React.FC<ListDrawerProps> = (props) => {
header={false}
gutter={false}
>
<ListDrawerContent {...props} />
<ListDrawerContent
{...props}
/>
</Drawer>
);
};
export const useListDrawer: UseListDrawer = ({
collectionSlugs,
collectionSlugs: collectionSlugsFromProps,
uploads,
selectedCollection,
filterOptions,
}) => {
const { collections } = useConfig();
const drawerDepth = useEditDepth();
const uuid = useId();
const { modalState, toggleModal, closeModal, openModal } = useModal();
const [isOpen, setIsOpen] = useState(false);
const [collectionSlugs, setCollectionSlugs] = useState(collectionSlugsFromProps);
const drawerSlug = formatListDrawerSlug({
depth: drawerDepth,
uuid,
@@ -73,6 +79,18 @@ export const useListDrawer: UseListDrawer = ({
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen));
}, [modalState, drawerSlug]);
useEffect(() => {
if (!collectionSlugs || collectionSlugs.length === 0) {
const filteredCollectionSlugs = collections.filter(({ upload }) => {
if (uploads) {
return Boolean(upload) === true;
}
return true;
});
setCollectionSlugs(filteredCollectionSlugs.map(({ slug }) => slug));
}
}, [collectionSlugs, uploads, collections]);
const toggleDrawer = useCallback(() => {
toggleModal(drawerSlug);
}, [toggleModal, drawerSlug]);

View File

@@ -22,7 +22,7 @@ export type ListTogglerProps = HTMLAttributes<HTMLButtonElement> & {
}
export type UseListDrawer = (args: {
collectionSlugs: string[]
collectionSlugs?: string[]
selectedCollection?: string
uploads?: boolean // finds all collections with upload: true
filterOptions?: FilterOptionsResult

View File

@@ -2,9 +2,14 @@
.clickable-arrow {
cursor: pointer;
transform: rotate(-90deg);
&--left {
&--right {
.icon {
transform: rotate(-90deg);
}
}
&--left .icon {
transform: rotate(90deg);
}

View File

@@ -18,6 +18,10 @@
}
}
.clickable-arrow--right {
margin-right: base(.25);
}
.clickable-arrow,
&__page {
@extend %btn-reset;

View File

@@ -93,16 +93,7 @@ const Pagination: React.FC<Props> = (props) => {
}
// Add prev and next arrows based on necessity
nodes.push({
type: 'ClickableArrow',
props: {
updatePage: () => updatePage(prevPage),
isDisabled: !hasPrevPage,
direction: 'left',
},
});
nodes.push({
nodes.unshift({
type: 'ClickableArrow',
props: {
updatePage: () => updatePage(nextPage),
@@ -111,6 +102,15 @@ const Pagination: React.FC<Props> = (props) => {
},
});
nodes.unshift({
type: 'ClickableArrow',
props: {
updatePage: () => updatePage(prevPage),
isDisabled: !hasPrevPage,
direction: 'left',
},
});
return (
<div className={baseClass}>
{nodes.map((node, i) => {

View File

@@ -44,7 +44,7 @@ const buildColumns = ({
return [...acc, field];
}, collection.fields);
const flattenedFields = flattenFields(combinedFields);
const flattenedFields = flattenFields(combinedFields, true);
// sort the fields to the order of activeColumns
const sortedFields = flattenedFields.sort((a, b) => {

View File

@@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Props } from './types';
import Thumbnail from '../Thumbnail';
@@ -28,6 +28,8 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
alignLabel && `${baseClass}--align-label-${alignLabel}`,
].filter(Boolean).join(' ');
const title: any = doc?.[collection?.admin?.useAsTitle] || doc?.filename || `[${t('untitled')}]`;
return (
<div
className={classes}
@@ -45,12 +47,7 @@ export const ThumbnailCard: React.FC<Props> = (props) => {
)}
</div>
<div className={`${baseClass}__label`}>
{label && label}
{!label && doc && (
<Fragment>
{typeof doc?.filename === 'string' ? doc?.filename : `[${t('untitled')}]`}
</Fragment>
)}
{label || title}
</div>
</div>
);

View File

@@ -1,15 +1,13 @@
@import '../../../scss/styles.scss';
$caretSize: 6;
.tooltip {
--caret-size: 6px;
opacity: 0;
background-color: var(--theme-elevation-800);
position: absolute;
z-index: 2;
bottom: 100%;
left: 50%;
transform: translate3d(-50%, calc(#{$caretSize}px * -1), 0);
padding: base(.2) base(.4);
color: var(--theme-elevation-0);
line-height: base(.75);
@@ -22,14 +20,12 @@ $caretSize: 6;
content: ' ';
display: block;
position: absolute;
bottom: 0;
left: 50%;
transform: translate3d(-50%, 100%, 0);
width: 0;
height: 0;
border-left: #{$caretSize}px solid transparent;
border-right: #{$caretSize}px solid transparent;
border-top: #{$caretSize}px solid var(--theme-elevation-800);
border-left: var(--caret-size) solid transparent;
border-right: var(--caret-size) solid transparent;
}
&--show {
@@ -39,6 +35,26 @@ $caretSize: 6;
cursor: default;
}
&--position-top {
bottom: 100%;
transform: translate3d(-50%, calc(var(--caret-size) * -1), 0);
&::after {
bottom: 1px;
border-top: var(--caret-size) solid var(--theme-elevation-800);
}
}
&--position-bottom {
top: 100%;
transform: translate3d(-50%, var(--caret-size), 0);
&::after {
bottom: calc(100% + var(--caret-size) - 1px);
border-bottom: var(--caret-size) solid var(--theme-elevation-800);
}
}
@include mid-break {
display: none;
}

View File

@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import { Props } from './types';
import useIntersect from '../../../hooks/useIntersect';
import './index.scss';
@@ -9,9 +10,18 @@ const Tooltip: React.FC<Props> = (props) => {
children,
show: showFromProps = true,
delay = 350,
boundingRef,
} = props;
const [show, setShow] = React.useState(showFromProps);
const [position, setPosition] = React.useState<'top' | 'bottom'>('top');
const [ref, intersectionEntry] = useIntersect({
threshold: 0,
rootMargin: '-145px 0px 0px 100px',
root: boundingRef?.current || null,
});
useEffect(() => {
let timerId: NodeJS.Timeout;
@@ -30,16 +40,35 @@ const Tooltip: React.FC<Props> = (props) => {
};
}, [showFromProps, delay]);
useEffect(() => {
setPosition(intersectionEntry?.isIntersecting ? 'top' : 'bottom');
}, [intersectionEntry]);
return (
<aside
className={[
'tooltip',
className,
show && 'tooltip--show',
].filter(Boolean).join(' ')}
>
{children}
</aside>
<React.Fragment>
<aside
ref={ref}
className={[
'tooltip',
className,
'tooltip--position-top',
].filter(Boolean).join(' ')}
aria-hidden="true"
>
{children}
</aside>
<aside
className={[
'tooltip',
className,
show && 'tooltip--show',
`tooltip--position-${position}`,
].filter(Boolean).join(' ')}
>
{children}
</aside>
</React.Fragment>
);
};

View File

@@ -3,4 +3,5 @@ export type Props = {
children: React.ReactNode
show?: boolean
delay?: number
boundingRef?: React.RefObject<HTMLElement>
}

View File

@@ -31,7 +31,11 @@ export const createRelationMap: CreateRelationMap = ({
const add = (relation: string, id: unknown) => {
if (((typeof id === 'string') || typeof id === 'number') && typeof relation === 'string') {
relationMap[relation].push(id);
if (relationMap[relation]) {
relationMap[relation].push(id);
} else {
relationMap[relation] = [id];
}
}
};

View File

@@ -81,6 +81,21 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
requests.post(`${serverURL}${api}/${userSlug}/logout`);
}, [serverURL, api, userSlug]);
const refreshPermissions = useCallback(async () => {
const request = await requests.get(`${serverURL}${api}/access`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) {
const json: Permissions = await request.json();
setPermissions(json);
} else {
throw new Error("Fetching permissions failed with status code " + request.status);
}
}, [serverURL, api, i18n]);
// On mount, get user and set
useEffect(() => {
const fetchMe = async () => {
@@ -117,21 +132,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// When user changes, get new access
useEffect(() => {
async function getPermissions() {
const request = await requests.get(`${serverURL}${api}/access`, {
headers: {
'Accept-Language': i18n.language,
},
});
if (request.status === 200) {
const json: Permissions = await request.json();
setPermissions(json);
}
}
if (id) {
getPermissions();
refreshPermissions();
}
}, [i18n, id, api, serverURL]);
@@ -174,6 +176,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
user,
logOut,
refreshCookie,
refreshPermissions,
permissions,
setToken,
token: tokenInMemory,

View File

@@ -6,5 +6,6 @@ export type AuthContext<T = User> = {
refreshCookie: () => void
setToken: (token: string) => void
token?: string
refreshPermissions: () => Promise<void>
permissions?: Permissions
}

View File

@@ -4,6 +4,7 @@ import { useConfig } from '../Config';
import { Props } from './types';
import payloadFavicon from '../../../assets/images/favicon.svg';
import payloadOgImage from '../../../assets/images/og-image.png';
import useMountEffect from '../../../hooks/useMountEffect';
const Meta: React.FC<Props> = ({
description,
@@ -17,6 +18,13 @@ const Meta: React.FC<Props> = ({
const favicon = config.admin.meta.favicon ?? payloadFavicon;
const ogImage = config.admin.meta.ogImage ?? payloadOgImage;
useMountEffect(() => {
const faviconElement = document.querySelector<HTMLLinkElement>('link[data-placeholder-favicon]');
if (faviconElement) {
faviconElement.remove();
}
});
return (
<Helmet
htmlAttributes={{

View File

@@ -59,6 +59,10 @@
.file-field__drop-zone {
border-color: var(--theme-success-500);
background: var(--theme-success-150);
* {
pointer-events: none;
}
}
}

View File

@@ -43,12 +43,15 @@ const usePayloadAPI: UsePayloadAPI = (url, options = {}) => {
});
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const response = await requests.get(`${url}${search}`, {
signal: abortController.signal,
headers: {
'Accept-Language': i18n.language,
},
@@ -62,8 +65,10 @@ const usePayloadAPI: UsePayloadAPI = (url, options = {}) => {
setData(json);
setIsLoading(false);
} catch (error) {
setIsError(true);
setIsLoading(false);
if (!abortController.signal.aborted) {
setIsError(true);
setIsLoading(false);
}
}
};
@@ -73,6 +78,10 @@ const usePayloadAPI: UsePayloadAPI = (url, options = {}) => {
setIsError(false);
setIsLoading(false);
}
return () => {
abortController.abort();
};
}, [url, locale, search, i18n.language]);
return [{ data, isLoading, isError }, { setParams }];

View File

@@ -4,6 +4,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel="icon" href="data:," data-placeholder-favicon/>
</head>
<body>

View File

@@ -140,7 +140,7 @@ async function update<TSlug extends keyof GeneratedTypes['collections']>(
entityConfig: collectionConfig,
req,
overrideAccess: true,
showHiddenFields,
showHiddenFields: true,
});
// /////////////////////////////////////

View File

@@ -331,25 +331,6 @@ export type Config = {
Dashboard?: React.ComponentType<any>;
};
};
/**
* Control pagination when querying collections.
*
* @see https://payloadcms.com/docs/queries/overview
*/
pagination?: {
/**
* Limit the number of documents that are displayed on 1 page in the list view
*
* @default 10
*/
defaultLimit?: number;
/**
* Suggest alternative options for the limit of documents on the list view
*
* @default [5, 10, 25, 50, 100]
*/
limits?: number[]
};
/** Customize the Webpack config that's used to generate the Admin panel. */
webpack?: (config: Configuration) => Configuration;
};

View File

@@ -130,6 +130,8 @@ export const promise = async ({
const pointDoc = siblingDoc[field.name] as Record<string, unknown>;
if (Array.isArray(pointDoc?.coordinates) && pointDoc.coordinates.length === 2) {
siblingDoc[field.name] = pointDoc.coordinates;
} else {
siblingDoc[field.name] = undefined;
}
break;

View File

@@ -56,6 +56,21 @@ export const promise = async <T>({
break;
}
case 'point': {
if (Array.isArray(siblingData[field.name])) {
siblingData[field.name] = (siblingData[field.name] as string[]).map((coordinate, i) => {
if (typeof coordinate === 'string') {
const value = siblingData[field.name][i] as string;
const trimmed = value.trim();
return (trimmed.length === 0) ? null : parseFloat(trimmed);
}
return coordinate;
});
}
break;
}
case 'checkbox': {
if (siblingData[field.name] === 'true') siblingData[field.name] = true;
if (siblingData[field.name] === 'false') siblingData[field.name] = false;

View File

@@ -8,10 +8,9 @@ import deepCopyObject from './deepCopyObject';
import { toWords } from './formatLabels';
import { SanitizedConfig } from '../config/types';
const nonOptionalFieldTypes = ['group', 'array', 'blocks'];
const propertyIsOptional = (field: Field) => {
return fieldAffectsData(field) && (('required' in field && field.required === true) || nonOptionalFieldTypes.includes(field.type));
return fieldAffectsData(field) && (('required' in field && field.required === true));
};
function getCollectionIDType(collections: SanitizedCollectionConfig[], slug: string): 'string' | 'number' {

View File

@@ -11,6 +11,7 @@ export const restrictedVersionsSlug = 'restricted-versions';
export const siblingDataSlug = 'sibling-data';
export const relyOnRequestHeadersSlug = 'rely-on-request-headers';
export const docLevelAccessSlug = 'doc-level-access';
export const hiddenFieldsSlug = 'hidden-fields';
const openAccess = {
create: () => true,
@@ -242,6 +243,46 @@ export default buildConfig({
},
],
},
{
slug: hiddenFieldsSlug,
access: openAccess,
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'partiallyHiddenGroup',
type: 'group',
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'value',
type: 'text',
hidden: true,
},
],
},
{
name: 'partiallyHiddenArray',
type: 'array',
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'value',
type: 'text',
hidden: true,
},
],
},
],
},
],
onInit: async (payload) => {
await payload.create({

View File

@@ -1,10 +1,9 @@
import mongoose from 'mongoose';
import payload from '../../src';
import type { Options as CreateOptions } from '../../src/collections/operations/local/create';
import { Forbidden } from '../../src/errors';
import type { PayloadRequest } from '../../src/types';
import { initPayloadTest } from '../helpers/configHelpers';
import { relyOnRequestHeadersSlug, requestHeaders, restrictedSlug, siblingDataSlug, slug } from './config';
import { hiddenFieldsSlug, relyOnRequestHeadersSlug, requestHeaders, restrictedSlug, siblingDataSlug, slug } from './config';
import type { Restricted, Post, RelyOnRequestHeader } from './payload-types';
import { firstArrayText, secondArrayText } from './shared';
@@ -34,7 +33,38 @@ describe('Access Control', () => {
await payload.mongoMemoryServer.stop();
});
it.todo('should properly prevent / allow public users from reading a restricted field');
it('should not affect hidden fields when patching data', async () => {
const doc = await payload.create({
collection: hiddenFieldsSlug,
data: {
partiallyHiddenArray: [{
name: 'public_name',
value: 'private_value',
}],
partiallyHiddenGroup: {
name: 'public_name',
value: 'private_value',
},
},
});
await payload.update({
collection: hiddenFieldsSlug,
id: doc.id,
data: {
title: 'Doc Title',
},
});
const updatedDoc = await payload.findByID({
collection: hiddenFieldsSlug,
id: doc.id,
showHiddenFields: true,
});
expect(updatedDoc.partiallyHiddenGroup.value).toEqual('private_value');
expect(updatedDoc.partiallyHiddenArray[0].value).toEqual('private_value');
});
it('should be able to restrict access based upon siblingData', async () => {
const { id } = await payload.create({
@@ -220,7 +250,7 @@ describe('Access Control', () => {
});
});
async function createDoc<Collection>(data: Partial<Collection>, overrideSlug = slug, options?: Partial<CreateOptions<Collection>>): Promise<Collection> {
async function createDoc<Collection>(data: Partial<Collection>, overrideSlug = slug, options?: Partial<Collection>): Promise<Collection> {
return payload.create({
...options,
collection: overrideSlug,

View File

@@ -5,11 +5,20 @@
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface Config {
collections: {
users: User;
posts: Post;
restricted: Restricted;
'read-only-collection': ReadOnlyCollection;
'restricted-versions': RestrictedVersion;
'sibling-data': SiblingDatum;
'rely-on-request-headers': RelyOnRequestHeader;
'doc-level-access': DocLevelAccess;
'hidden-fields': HiddenField;
};
globals: {};
}
export interface User {
id: string;
email?: string;
@@ -19,15 +28,12 @@ export interface User {
lockUntil?: string;
createdAt: string;
updatedAt: string;
password?: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: string;
restrictedField?: string;
group: {
group?: {
restrictedGroupText?: string;
};
restrictedRowText?: string;
@@ -35,43 +41,27 @@ export interface Post {
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted".
*/
export interface Restricted {
id: string;
name?: string;
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "read-only-collection".
*/
export interface ReadOnlyCollection {
id: string;
name?: string;
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "restricted-versions".
*/
export interface RestrictedVersion {
id: string;
name?: string;
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "sibling-data".
*/
export interface SiblingDatum {
id: string;
array: {
array?: {
allowPublicReadability?: boolean;
text?: string;
id?: string;
@@ -79,20 +69,12 @@ export interface SiblingDatum {
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "rely-on-request-headers".
*/
export interface RelyOnRequestHeader {
id: string;
name?: string;
createdAt: string;
updatedAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "doc-level-access".
*/
export interface DocLevelAccess {
id: string;
approvedForRemoval?: boolean;
@@ -101,3 +83,18 @@ export interface DocLevelAccess {
createdAt: string;
updatedAt: string;
}
export interface HiddenField {
id: string;
title?: string;
partiallyHiddenGroup?: {
name?: string;
value?: string;
};
partiallyHiddenArray?: {
name?: string;
value?: string;
id?: string;
}[];
createdAt: string;
updatedAt: string;
}

View File

@@ -9,6 +9,8 @@ import BeforeLogin from './components/BeforeLogin';
import AfterNavLinks from './components/AfterNavLinks';
import { slug, globalSlug } from './shared';
import Logout from './components/Logout';
import DemoUIFieldField from './components/DemoUIField/Field';
import DemoUIFieldCell from './components/DemoUIField/Cell';
export interface Post {
id: string;
@@ -83,7 +85,7 @@ export default buildConfig({
listSearchableFields: ['title', 'description', 'number'],
group: { en: 'One', es: 'Una' },
useAsTitle: 'title',
defaultColumns: ['id', 'number', 'title', 'description'],
defaultColumns: ['id', 'number', 'title', 'description', 'demoUIField'],
},
fields: [
{
@@ -111,6 +113,17 @@ export default buildConfig({
],
},
},
{
type: 'ui',
name: 'demoUIField',
label: 'Demo UI Field',
admin: {
components: {
Field: DemoUIFieldField,
Cell: DemoUIFieldCell,
},
},
},
],
},
{

View File

@@ -409,6 +409,12 @@ describe('admin', () => {
// ensure that the "number" column is still deselected
await expect(await page.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column').first()).not.toHaveClass('column-selector__column--active');
});
test('should render custom table cell component', async () => {
await createPost();
await page.goto(url.list);
await expect(await page.locator('table >> thead >> tr >> th >> text=Demo UI Field')).toBeVisible();
});
});
describe('pagination', () => {

View File

@@ -1,6 +1,7 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import path from 'path';
import payload from '../../src';
import { AdminUrlUtil } from '../helpers/adminUrlUtil';
import { initPayloadE2E } from '../helpers/configHelpers';
import { login, saveDocAndAssert } from '../helpers';
@@ -136,8 +137,26 @@ describe('fields', () => {
describe('point', () => {
let url: AdminUrlUtil;
beforeAll(() => {
let filledGroupPoint;
let emptyGroupPoint;
beforeAll(async () => {
url = new AdminUrlUtil(serverURL, pointFieldsSlug);
filledGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
point: [5, 5],
localized: [4, 2],
group: { point: [4, 2] },
},
});
emptyGroupPoint = await payload.create({
collection: pointFieldsSlug,
data: {
point: [5, 5],
localized: [3, -2],
group: {},
},
});
});
test('should save point', async () => {
@@ -161,6 +180,57 @@ describe('fields', () => {
await groupLatField.fill('-8');
await saveDocAndAssert(page);
await expect(await longField.getAttribute('value')).toEqual('9');
await expect(await latField.getAttribute('value')).toEqual('-2');
await expect(await localizedLongField.getAttribute('value')).toEqual('1');
await expect(await localizedLatField.getAttribute('value')).toEqual('-1');
await expect(await groupLongitude.getAttribute('value')).toEqual('3');
await expect(await groupLatField.getAttribute('value')).toEqual('-8');
});
test('should update point', async () => {
await page.goto(url.edit(emptyGroupPoint.id));
const longField = page.locator('#field-longitude-point');
await longField.fill('9');
const latField = page.locator('#field-latitude-point');
await latField.fill('-2');
const localizedLongField = page.locator('#field-longitude-localized');
await localizedLongField.fill('2');
const localizedLatField = page.locator('#field-latitude-localized');
await localizedLatField.fill('-2');
const groupLongitude = page.locator('#field-longitude-group__point');
await groupLongitude.fill('3');
const groupLatField = page.locator('#field-latitude-group__point');
await groupLatField.fill('-8');
await saveDocAndAssert(page);
await expect(await longField.getAttribute('value')).toEqual('9');
await expect(await latField.getAttribute('value')).toEqual('-2');
await expect(await localizedLongField.getAttribute('value')).toEqual('2');
await expect(await localizedLatField.getAttribute('value')).toEqual('-2');
await expect(await groupLongitude.getAttribute('value')).toEqual('3');
await expect(await groupLatField.getAttribute('value')).toEqual('-8');
});
test('should be able to clear a value point', async () => {
await page.goto(url.edit(filledGroupPoint.id));
const groupLongitude = page.locator('#field-longitude-group__point');
await groupLongitude.fill('');
const groupLatField = page.locator('#field-latitude-group__point');
await groupLatField.fill('');
await saveDocAndAssert(page);
await expect(await groupLongitude.getAttribute('value')).toEqual('');
await expect(await groupLatField.getAttribute('value')).toEqual('');
});
});

View File

@@ -0,0 +1,22 @@
import React, { useCallback } from 'react';
import { useAuth } from '../../src/admin/components/utilities/Auth';
import { Props } from '../../src/admin/components/views/Global/types';
import DefaultGlobalView from '../../src/admin/components/views/Global/Default';
const GlobalView: React.FC<Props> = (props) => {
const { onSave } = props;
const { refreshPermissions } = useAuth();
const modifiedOnSave = useCallback((...args) => {
onSave.call(null, ...args);
refreshPermissions();
}, [onSave, refreshPermissions]);
return (
<DefaultGlobalView
{...props}
onSave={modifiedOnSave}
/>
);
};
export default GlobalView;

View File

@@ -0,0 +1,53 @@
import { buildConfig } from '../buildConfig';
import { devUser } from '../credentials';
import GlobalViewWithRefresh from './GlobalViewWithRefresh';
export const pagesSlug = 'pages';
export default buildConfig({
globals: [
{
slug: 'settings',
fields: [
{
type: 'checkbox',
name: 'test',
label: 'Allow access to test global',
},
],
admin: {
components: {
views: {
Edit: GlobalViewWithRefresh,
},
},
},
},
{
slug: 'test',
fields: [],
access: {
read: async ({ req: { payload } }) => {
const access = await payload.findGlobal({ slug: 'settings' });
return access.test;
},
},
},
],
collections: [
{
slug: 'users',
auth: true,
fields: [],
},
],
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
});
},
});

View File

@@ -0,0 +1,35 @@
import { expect, Page, test } from '@playwright/test';
import { login } from '../helpers';
import { initPayloadE2E } from '../helpers/configHelpers';
const { beforeAll, describe } = test;
describe('refresh-permissions', () => {
let serverURL: string;
let page: Page;
beforeAll(async ({ browser }) => {
({ serverURL } = await initPayloadE2E(__dirname));
const context = await browser.newContext();
page = await context.newPage();
await login({ page, serverURL });
});
test('should show test global immediately after allowing access', async () => {
await page.goto(`${serverURL}/admin/globals/settings`);
// Ensure that we have loaded accesses by checking that settings collection
// at least is visible in the menu.
await expect(page.locator('#nav-global-settings')).toBeVisible();
// Test collection should be hidden at first.
await expect(page.locator('#nav-global-test')).toBeHidden();
// Allow access to test global.
await page.locator('.custom-checkbox:has(#field-test) button').click();
await page.locator('#action-save').click();
// Now test collection should appear in the menu.
await expect(page.locator('#nav-global-test')).toBeVisible();
});
});