diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a3d861c6..658b54b7f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## [1.1.3](https://github.com/payloadcms/payload/compare/v1.1.2...v1.1.3) (2022-09-16) + + +### Bug Fixes + +* adjust prevPage and nextPage graphql typing ([#1140](https://github.com/payloadcms/payload/issues/1140)) ([b3bb421](https://github.com/payloadcms/payload/commit/b3bb421c6ca4176974488b3270384386a151560c)) +* duplicate with relationships ([eabb981](https://github.com/payloadcms/payload/commit/eabb981243e005facb5fff6d9222903d4704ca55)) + +## [1.1.2](https://github.com/payloadcms/payload/compare/v1.1.1...v1.1.2) (2022-09-14) + + +### Bug Fixes + +* resize images without local storage ([1496679](https://github.com/payloadcms/payload/commit/14966796ae0d0bcff8cb56b62e3a21c2de2176da)) +* resize images without local storage ([7b756f3](https://github.com/payloadcms/payload/commit/7b756f3421f02d1ff55374a72396e15e9f3e23d7)) + +## [1.1.1](https://github.com/payloadcms/payload/compare/v1.1.0...v1.1.1) (2022-09-13) + + +### Bug Fixes + +* conditions on collapsible fields ([9c4f2b6](https://github.com/payloadcms/payload/commit/9c4f2b68b07bbdd2ac9a6dee280f50379638fc50)) +* dashboard links to globals ([dcc8dad](https://github.com/payloadcms/payload/commit/dcc8dad53b006f86e93150f9439eafc8d9e01d79)) + # [1.1.0](https://github.com/payloadcms/payload/compare/v1.0.36...v1.1.0) (2022-09-13) diff --git a/docs/admin/components.mdx b/docs/admin/components.mdx index 6e82397e8a..9950df5e40 100644 --- a/docs/admin/components.mdx +++ b/docs/admin/components.mdx @@ -97,11 +97,51 @@ All Payload fields support the ability to swap in your own React components. So, **Fields support the following custom components:** -| Component | Description | -| --------------- | -------------| -| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. | -| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. | -| **`Field`** | Swap out the field itself within all `Edit` views. | +| Component | Description | +| --------------- |------------------------------------------------------------------------------------------------------------------------------| +| **`Filter`** | Override the text input that is presented in the `List` view when a user is filtering documents by the customized field. | +| **`Cell`** | Used in the `List` view's table to represent a table-based preview of the data stored in the field. [More](#cell-component) | +| **`Field`** | Swap out the field itself within all `Edit` views. [More](#field-component) | + +## Cell Component + +These are the props that will be passed to your custom Cell to use in your own components. + +| Property | Description | +|--------------|-------------------------------------------------------------------| +| **`field`** | An object that includes the field configuration. | +| **`colIndex`** | A unique number for the column in the list. | +| **`collection`** | An object with the config of the collection that the field is in. | +| **`cellData`** | The data for the field that the cell represents. | +| **`rowData`** | An object with all the field values for the row. | + +#### Example + +```tsx +import React from 'react'; +import './index.scss'; +const baseClass = 'custom-cell'; + +const CustomCell: React.FC = (props) => { + const { + field, + colIndex, + collection, + cellData, + rowData, + } = props; + + return ( + + { cellData } + + ); +}; +``` + +## Field Component + +When writing your own custom components you can make use of a number of hooks to set data, get reactive changes to other fields, get the id of the document or interact with a context from a custom provider. ### Sending and receiving values from the form diff --git a/docs/admin/webpack.mdx b/docs/admin/webpack.mdx index b48060b87d..c696a678a6 100644 --- a/docs/admin/webpack.mdx +++ b/docs/admin/webpack.mdx @@ -155,6 +155,11 @@ export default {}; Now, when Webpack sees that you're attempting to import your `createStripeSubscriptionPath` file, it'll disregard that actual file and load your mock file instead. Not only will your Admin panel now bundle successfully, you will have optimized its filesize by removing unnecessary code! And you might have learned something about Webpack, too. + + Tip:
+ If changes to your Webpack aliases are not surfacing, they might be [cached](https://webpack.js.org/configuration/cache/) in `node_modules/.cache/webpack`. Try deleting that folder and restarting your server. +
+ ## Admin environment vars diff --git a/docs/fields/ui.mdx b/docs/fields/ui.mdx index 89737a40f7..657f3466ec 100644 --- a/docs/fields/ui.mdx +++ b/docs/fields/ui.mdx @@ -23,12 +23,12 @@ With this field, you can also inject custom `Cell` components that appear as add ### Config -| Option | Description | -| ---------------------------- | ----------- | -| **`name`** * | A unique identifier for this field. | -| **`label`** | Human-readable label for this UI field. | -| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. | -| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. | +| Option | Description | +| ---------------------------- |-------------------------------------------------------------------------------------------------------------------| +| **`name`** * | A unique identifier for this field. | +| **`label`** | Human-readable label for this UI field. | +| **`admin.components.Field`** | React component to be rendered for this field within the Edit view. [More](/admin/components/#field-component) | +| **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More](/admin/components/#field-component) | *\* An asterisk denotes that a property is required.* diff --git a/docs/plugins/overview.mdx b/docs/plugins/overview.mdx index 180d19f755..82578a21e4 100644 --- a/docs/plugins/overview.mdx +++ b/docs/plugins/overview.mdx @@ -27,7 +27,7 @@ Writing plugins is no more complex than writing regular JavaScript. If you know ### How to install plugins -The base Payload config allows for a `plugins` property which takes an `array` of [`Plugin`s](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21). +The base Payload config allows for a `plugins` property which takes an `array` of [`Plugins`](https://github.com/payloadcms/payload/blob/master/src/config/types.ts#L21). ```js import { buildConfig } from 'payload/config'; @@ -134,7 +134,7 @@ const addLastModified: Plugin = (incomingConfig: Config): Config => { export default addLastModified; ``` -#### Available Plugins +### Available Plugins You can discover existing plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin). diff --git a/docs/rest-api/overview.mdx b/docs/rest-api/overview.mdx index 57d8dad1db..005feee871 100644 --- a/docs/rest-api/overview.mdx +++ b/docs/rest-api/overview.mdx @@ -86,6 +86,7 @@ Each endpoint object needs to have: | **`path`** | A string for the endpoint route after the collection or globals slug | | **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' | | **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Express](https://expressjs.com/en/guide/routing.html#route-handlers) | +| **`root`** | When `true`, defines the endpoint on the root Express app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload config, endpoints defined on `collections` or `globals` cannot be root. | Example: diff --git a/package.json b/package.json index f20f6aed18..efb9ffe9db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "payload", - "version": "1.1.0", + "version": "1.1.3", "description": "Node, React and MongoDB Headless CMS and Application Framework", "license": "MIT", "author": { @@ -31,7 +31,7 @@ "build:tsc": "tsc --p tsconfig.admin.json && tsc --p tsconfig.server.json", "build:components": "webpack --config dist/webpack/components.config.js", "build": "yarn copyfiles && yarn build:tsc && yarn build:components", - "build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec 'yarn build:tsc'", + "build:watch": "nodemon --watch 'src/**' --ext 'ts,tsx' --exec \"yarn build:tsc\"", "dev": "nodemon", "dev:generate-types": "node ./test/generateTypes.js", "pretest": "yarn build", @@ -188,7 +188,6 @@ "webpack-hot-middleware": "^2.25.0" }, "devDependencies": { - "@bahmutov/cy-api": "^2.1.3", "@playwright/test": "^1.23.1", "@release-it/conventional-changelog": "^2.0.0", "@testing-library/jest-dom": "^5.11.4", diff --git a/src/admin/components/elements/DuplicateDocument/index.tsx b/src/admin/components/elements/DuplicateDocument/index.tsx index b1cc135135..93dad6ddf0 100644 --- a/src/admin/components/elements/DuplicateDocument/index.tsx +++ b/src/admin/components/elements/DuplicateDocument/index.tsx @@ -32,9 +32,11 @@ const Duplicate: React.FC = ({ slug, collection, id }) => { return; } - const create = async (locale?: string): Promise => { - const localeParam = locale ? `locale=${locale}` : ''; - const response = await requests.get(`${serverURL}${api}/${slug}/${id}?${localeParam}`); + const create = async (locale = ''): Promise => { + const response = await requests.get(`${serverURL}${api}/${slug}/${id}`, { + locale, + depth: 0, + }); const data = await response.json(); const result = await requests.post(`${serverURL}${api}/${slug}`, { headers: { @@ -59,7 +61,10 @@ const Duplicate: React.FC = ({ slug, collection, id }) => { .filter((locale) => locale !== localization.defaultLocale) .forEach(async (locale) => { if (!abort) { - const res = await requests.get(`${serverURL}${api}/${slug}/${id}?locale=${locale}`); + const res = await requests.get(`${serverURL}${api}/${slug}/${id}`, { + locale, + depth: 0, + }); const localizedDoc = await res.json(); const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, { headers: { diff --git a/src/admin/components/forms/RenderFields/index.tsx b/src/admin/components/forms/RenderFields/index.tsx index 9c07df0c79..468b83adfc 100644 --- a/src/admin/components/forms/RenderFields/index.tsx +++ b/src/admin/components/forms/RenderFields/index.tsx @@ -88,7 +88,7 @@ const RenderFields: React.FC = (props) => { DefaultComponent={FieldComponent} componentProps={{ ...field, - path: field.path || (isFieldAffectingData ? field.name : undefined), + path: field.path || (isFieldAffectingData ? field.name : ''), fieldTypes, admin: { ...(field.admin || {}), diff --git a/src/admin/components/forms/RenderFields/types.ts b/src/admin/components/forms/RenderFields/types.ts index b8b2df0089..3ee4d6b298 100644 --- a/src/admin/components/forms/RenderFields/types.ts +++ b/src/admin/components/forms/RenderFields/types.ts @@ -6,7 +6,7 @@ export type Props = { className?: string readOnly?: boolean forceRender?: boolean - permissions?: { + permissions?: FieldPermissions | { [field: string]: FieldPermissions } filter?: (field: Field) => boolean diff --git a/src/admin/components/forms/field-types/Collapsible/index.tsx b/src/admin/components/forms/field-types/Collapsible/index.tsx index a48adaee52..23d9ac538d 100644 --- a/src/admin/components/forms/field-types/Collapsible/index.tsx +++ b/src/admin/components/forms/field-types/Collapsible/index.tsx @@ -75,7 +75,7 @@ const CollapsibleField: React.FC = (props) => { ({ ...field, diff --git a/src/admin/components/forms/field-types/Row/index.tsx b/src/admin/components/forms/field-types/Row/index.tsx index c5878dd542..88a1542c8c 100644 --- a/src/admin/components/forms/field-types/Row/index.tsx +++ b/src/admin/components/forms/field-types/Row/index.tsx @@ -28,7 +28,7 @@ const Row: React.FC = (props) => { ({ ...field, diff --git a/src/admin/components/forms/field-types/Tabs/index.tsx b/src/admin/components/forms/field-types/Tabs/index.tsx index b6646eced1..84bafbddfe 100644 --- a/src/admin/components/forms/field-types/Tabs/index.tsx +++ b/src/admin/components/forms/field-types/Tabs/index.tsx @@ -71,7 +71,7 @@ const TabsField: React.FC = (props) => { key={String(activeTab.label)} forceRender readOnly={readOnly} - permissions={permissions?.fields} + permissions={tabHasName(activeTab) ? permissions[activeTab.name].fields : permissions} fieldTypes={fieldTypes} fieldSchema={activeTab.fields.map((field) => ({ ...field, diff --git a/src/admin/components/forms/withCondition/index.tsx b/src/admin/components/forms/withCondition/index.tsx index f9472992fe..a4a1eb1f3c 100644 --- a/src/admin/components/forms/withCondition/index.tsx +++ b/src/admin/components/forms/withCondition/index.tsx @@ -28,7 +28,7 @@ const withCondition =

>(Field: React.Component path?: string }; - const path = pathFromProps || name; + const path = typeof pathFromProps === 'string' ? pathFromProps : name; const { getData, getSiblingData, getField, dispatchFields } = useWatchForm(); diff --git a/src/admin/components/views/Dashboard/Default.tsx b/src/admin/components/views/Dashboard/Default.tsx index 93df778904..e12ff63039 100644 --- a/src/admin/components/views/Dashboard/Default.tsx +++ b/src/admin/components/views/Dashboard/Default.tsx @@ -82,7 +82,7 @@ const Dashboard: React.FC = (props) => { if (type === EntityType.global) { title = entity.label; - onClick = () => push({ pathname: `${admin}/globals/${global.slug}` }); + onClick = () => push({ pathname: `${admin}/globals/${entity.slug}` }); } return ( diff --git a/src/auth/operations/access.ts b/src/auth/operations/access.ts index a9dfbc6a4b..40cd096f4e 100644 --- a/src/auth/operations/access.ts +++ b/src/auth/operations/access.ts @@ -1,6 +1,7 @@ import { PayloadRequest } from '../../express/types'; import { Permissions } from '../types'; import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit'; +import { tabHasName } from '../../fields/config/types'; const allOperations = ['create', 'read', 'update', 'delete']; @@ -66,7 +67,12 @@ async function accessOperation(args: Arguments): Promise { executeFieldPolicies(updatedObj, field.fields, operation); } else if (field.type === 'tabs') { field.tabs.forEach((tab) => { - executeFieldPolicies(updatedObj, tab.fields, operation); + if (tabHasName(tab)) { + if (!updatedObj[tab.name]) updatedObj[tab.name] = { fields: {} }; + executeFieldPolicies(updatedObj[tab.name].fields, tab.fields, operation); + } else { + executeFieldPolicies(updatedObj, tab.fields, operation); + } }); } }); diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index c8d4ec15b6..f9aa0c41b3 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -226,7 +226,7 @@ export type CollectionConfig = { /** * Custom rest api endpoints */ - endpoints?: Endpoint[] + endpoints?: Omit[] /** * Access control */ diff --git a/src/collections/graphql/init.ts b/src/collections/graphql/init.ts index ddbc3b0a36..3f30988fb3 100644 --- a/src/collections/graphql/init.ts +++ b/src/collections/graphql/init.ts @@ -119,7 +119,7 @@ function initCollectionsGraphQL(payload: Payload): void { singularLabel, ); - if (collection.config.auth) { + if (collection.config.auth && !collection.config.auth.disableLocalStrategy) { fields.push({ name: 'password', label: 'Password', @@ -274,21 +274,23 @@ function initCollectionsGraphQL(payload: Payload): void { } if (collection.config.auth) { + const authFields: Field[] = collection.config.auth.disableLocalStrategy ? [] : [{ + name: 'email', + type: 'email', + required: true, + }] collection.graphQL.JWT = buildObjectType({ payload, name: formatName(`${slug}JWT`), - fields: collection.config.fields.filter((field) => fieldAffectsData(field) && field.saveToJWT).concat([ - { - name: 'email', - type: 'email', - required: true, - }, + fields: [ + ...collection.config.fields.filter((field) => fieldAffectsData(field) && field.saveToJWT), + ...authFields, { name: 'collection', type: 'text', required: true, - }, - ]), + } + ], parentName: formatName(`${slug}JWT`), }); diff --git a/src/collections/init.ts b/src/collections/init.ts index fb94bdde98..736e42590d 100644 --- a/src/collections/init.ts +++ b/src/collections/init.ts @@ -113,7 +113,7 @@ export default function registerCollections(ctx: Payload): void { } const endpoints = buildEndpoints(collection); - mountEndpoints(router, endpoints); + mountEndpoints(ctx.express, router, endpoints); ctx.router.use(`/${slug}`, router); } diff --git a/src/config/schema.ts b/src/config/schema.ts index 14a815e876..83b8c6f47a 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -8,6 +8,7 @@ const component = joi.alternatives().try( export const endpointsSchema = joi.array().items(joi.object({ path: joi.string(), method: joi.string().valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'), + root: joi.bool(), handler: joi.alternatives().try( joi.array().items(joi.func()), joi.func(), diff --git a/src/config/types.ts b/src/config/types.ts index 80291c03b6..e8517a6ac9 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -79,16 +79,19 @@ export type AccessResult = boolean | Where; */ export type Access = (args?: any) => AccessResult | Promise; -export interface PayloadHandler {( +export interface PayloadHandler { + ( req: PayloadRequest, res: Response, next: NextFunction, - ): void } + ): void +} export type Endpoint = { path: string method: 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'connect' | 'options' | string handler: PayloadHandler | PayloadHandler[] + root?: boolean } export type AdminView = React.ComponentType<{ user: User, canAccessAdmin: boolean }> diff --git a/src/express/mountEndpoints.ts b/src/express/mountEndpoints.ts index eb0b547e5f..8ac026d461 100644 --- a/src/express/mountEndpoints.ts +++ b/src/express/mountEndpoints.ts @@ -1,9 +1,13 @@ -import { Router } from 'express'; +import { Express, Router } from 'express'; import { Endpoint } from '../config/types'; -function mountEndpoints(router: Router, endpoints: Endpoint[]): void { +function mountEndpoints(express: Express, router: Router, endpoints: Endpoint[]): void { endpoints.forEach((endpoint) => { - router[endpoint.method](endpoint.path, endpoint.handler); + if (!endpoint.root) { + router[endpoint.method](endpoint.path, endpoint.handler); + } else { + express[endpoint.method](endpoint.path, endpoint.handler); + } }); } diff --git a/src/globals/config/types.ts b/src/globals/config/types.ts index c46d8de9ab..14a14a0552 100644 --- a/src/globals/config/types.ts +++ b/src/globals/config/types.ts @@ -56,7 +56,7 @@ export type GlobalConfig = { beforeRead?: BeforeReadHook[] afterRead?: AfterReadHook[] } - endpoints?: Endpoint[], + endpoints?: Omit[], access?: { read?: Access; readDrafts?: Access; diff --git a/src/globals/init.ts b/src/globals/init.ts index e8732291bd..a82d946d92 100644 --- a/src/globals/init.ts +++ b/src/globals/init.ts @@ -48,7 +48,7 @@ export default function initGlobals(ctx: Payload): void { const { slug } = global; const endpoints = buildEndpoints(global); - mountEndpoints(router, endpoints); + mountEndpoints(ctx.express, router, endpoints); ctx.router.use(`/globals/${slug}`, router); }); diff --git a/src/graphql/schema/buildPaginatedListType.ts b/src/graphql/schema/buildPaginatedListType.ts index 20c4a8f7cd..76b4e99056 100644 --- a/src/graphql/schema/buildPaginatedListType.ts +++ b/src/graphql/schema/buildPaginatedListType.ts @@ -14,8 +14,8 @@ const buildPaginatedListType = (name, docType) => new GraphQLObjectType({ pagingCounter: { type: GraphQLInt }, hasPrevPage: { type: GraphQLBoolean }, hasNextPage: { type: GraphQLBoolean }, - prevPage: { type: GraphQLBoolean }, - nextPage: { type: GraphQLBoolean }, + prevPage: { type: GraphQLInt }, + nextPage: { type: GraphQLInt }, }, }); diff --git a/src/init.ts b/src/init.ts index 1f5d7daeb8..2a3dd916fb 100644 --- a/src/init.ts +++ b/src/init.ts @@ -106,7 +106,7 @@ export const init = (payload: Payload, options: InitOptions): void => { initGraphQLPlayground(payload); } - mountEndpoints(payload.router, payload.config.endpoints); + mountEndpoints(options.express, payload.router, payload.config.endpoints); // Bind router to API payload.express.use(payload.config.routes.api, payload.router); diff --git a/src/uploads/canResizeImage.ts b/src/uploads/canResizeImage.ts new file mode 100644 index 0000000000..6bb6f63d5d --- /dev/null +++ b/src/uploads/canResizeImage.ts @@ -0,0 +1,3 @@ +export default function canResizeImage(mimeType: string): boolean { + return ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].indexOf(mimeType) > -1; +} diff --git a/src/uploads/imageResizer.ts b/src/uploads/imageResizer.ts index d3cd3bd9a8..de32629529 100644 --- a/src/uploads/imageResizer.ts +++ b/src/uploads/imageResizer.ts @@ -1,12 +1,12 @@ -import fs from 'fs'; -import sharp from 'sharp'; -import sanitize from 'sanitize-filename'; import { fromBuffer } from 'file-type'; -import { ProbedImageSize } from './getImageSize'; -import fileExists from './fileExists'; +import fs from 'fs'; +import sanitize from 'sanitize-filename'; +import sharp from 'sharp'; import { SanitizedCollectionConfig } from '../collections/config/types'; -import { FileSizes, ImageSize } from './types'; import { PayloadRequest } from '../express/types'; +import fileExists from './fileExists'; +import { ProbedImageSize } from './getImageSize'; +import { FileSizes, ImageSize } from './types'; type Args = { req: PayloadRequest @@ -50,7 +50,7 @@ export default async function resizeAndSave({ const sizes = imageSizes .filter((desiredSize) => needsResize(desiredSize, dimensions)) .map(async (desiredSize) => { - let resized = await sharp(file).resize(desiredSize); + let resized = sharp(file).resize(desiredSize); if (desiredSize.formatOptions) { resized = resized.toFormat(desiredSize.formatOptions.format, desiredSize.formatOptions.options); diff --git a/src/uploads/uploadFile.ts b/src/uploads/uploadFile.ts index 3c3b2b3705..db35a958a5 100644 --- a/src/uploads/uploadFile.ts +++ b/src/uploads/uploadFile.ts @@ -1,18 +1,18 @@ +import { fromBuffer } from 'file-type'; import mkdirp from 'mkdirp'; import path from 'path'; -import sharp, { Sharp } from 'sharp'; -import { fromBuffer } from 'file-type'; import sanitize from 'sanitize-filename'; -import { SanitizedConfig } from '../config/types'; +import sharp, { Sharp } from 'sharp'; import { Collection } from '../collections/config/types'; +import { SanitizedConfig } from '../config/types'; import { FileUploadError, MissingFile } from '../errors'; import { PayloadRequest } from '../express/types'; -import { FileData } from './types'; -import saveBufferToFile from './saveBufferToFile'; +import getImageSize, { ProbedImageSize } from './getImageSize'; import getSafeFileName from './getSafeFilename'; -import getImageSize from './getImageSize'; import resizeAndSave from './imageResizer'; -import isImage from './isImage'; +import saveBufferToFile from './saveBufferToFile'; +import { FileData } from './types'; +import canResizeImage from './canResizeImage'; type Args = { config: SanitizedConfig, @@ -59,54 +59,53 @@ const uploadFile = async ({ if (file) { try { + const shouldResize = canResizeImage(file.mimetype); let fsSafeName: string; - let fileBuffer: Buffer; - let mimeType: string; - let fileSize: number; - - if (!disableLocalStorage) { - let resized: Sharp | undefined; + let resized: Sharp | undefined; + let dimensions: ProbedImageSize; + if (shouldResize) { if (resizeOptions) { - resized = sharp(file.data).resize(resizeOptions); + resized = sharp(file.data) + .resize(resizeOptions); } if (formatOptions) { resized = (resized ?? sharp(file.data)).toFormat(formatOptions.format, formatOptions.options); } - fileBuffer = resized ? (await resized.toBuffer()) : file.data; - const { mime, ext } = await fromBuffer(fileBuffer); - mimeType = mime; - fileSize = fileBuffer.length; - const baseFilename = sanitize(file.name.substring(0, file.name.lastIndexOf('.')) || file.name); - fsSafeName = `${baseFilename}.${ext}`; + dimensions = await getImageSize(file); + fileData.width = dimensions.width; + fileData.height = dimensions.height; + } - if (!overwriteExistingFiles) { - fsSafeName = await getSafeFileName(Model, staticPath, fsSafeName); - } + const fileBuffer = resized ? (await resized.toBuffer()) : file.data; + const { mime, ext } = await fromBuffer(fileBuffer) ?? { mime: file.mimetype, ext: file.name.split('.').pop() }; + const fileSize = fileBuffer.length; + const baseFilename = sanitize(file.name.substring(0, file.name.lastIndexOf('.')) || file.name); + fsSafeName = `${baseFilename}.${ext}`; + + if (!overwriteExistingFiles) { + fsSafeName = await getSafeFileName(Model, staticPath, fsSafeName); + } + + if (!disableLocalStorage) { await saveBufferToFile(fileBuffer, `${staticPath}/${fsSafeName}`); } fileData.filename = fsSafeName || (!overwriteExistingFiles ? await getSafeFileName(Model, staticPath, file.name) : file.name); fileData.filesize = fileSize || file.size; - fileData.mimeType = mimeType || (await fromBuffer(file.data)).mime; + fileData.mimeType = mime || (await fromBuffer(file.data)).mime; - if (isImage(file.mimetype)) { - const dimensions = await getImageSize(file); - fileData.width = dimensions.width; - fileData.height = dimensions.height; - - if (Array.isArray(imageSizes) && file.mimetype !== 'image/svg+xml') { - req.payloadUploadSizes = {}; - fileData.sizes = await resizeAndSave({ - req, - file: file.data, - dimensions, - staticPath, - config: collectionConfig, - savedFilename: fsSafeName, - mimeType: fileData.mimeType, - }); - } + if (Array.isArray(imageSizes) && shouldResize) { + req.payloadUploadSizes = {}; + fileData.sizes = await resizeAndSave({ + req, + file: file.data, + dimensions, + staticPath, + config: collectionConfig, + savedFilename: fsSafeName || file.name, + mimeType: fileData.mimeType, + }); } } catch (err) { console.error(err); diff --git a/test/access-control/config.ts b/test/access-control/config.ts index 3cc59faf8a..8eed5be93a 100644 --- a/test/access-control/config.ts +++ b/test/access-control/config.ts @@ -25,7 +25,7 @@ const PublicReadabilityAccess: FieldAccess = ({ req: { user }, siblingData }) => return false; }; -export const requestHeaders = {authorization: 'Bearer testBearerToken'}; +export const requestHeaders = { authorization: 'Bearer testBearerToken' }; const UseRequestHeadersAccess: FieldAccess = ({ req: { headers } }) => { return !!headers && headers.authorization === requestHeaders.authorization; }; @@ -47,6 +47,50 @@ export default buildConfig({ update: () => false, }, }, + { + type: 'group', + name: 'group', + fields: [ + { + name: 'restrictedGroupText', + type: 'text', + access: { + read: () => false, + update: () => false, + create: () => false, + }, + }, + ], + }, + { + type: 'row', + fields: [ + { + name: 'restrictedRowText', + type: 'text', + access: { + read: () => false, + update: () => false, + create: () => false, + }, + }, + ], + }, + { + type: 'collapsible', + label: 'Access', + fields: [ + { + name: 'restrictedCollapsibleText', + type: 'text', + access: { + read: () => false, + update: () => false, + create: () => false, + }, + }, + ], + }, ], }, { diff --git a/test/access-control/e2e.spec.ts b/test/access-control/e2e.spec.ts index f248966fa0..afbdbe64f9 100644 --- a/test/access-control/e2e.spec.ts +++ b/test/access-control/e2e.spec.ts @@ -44,7 +44,31 @@ describe('access control', () => { await page.goto(url.edit(id)); - await expect(page.locator('input[name="restrictedField"]')).toHaveCount(0); + await expect(page.locator('#field-restrictedField')).toHaveCount(0); + }); + + test('field without read access inside a group should not show', async () => { + const { id } = await createDoc({ restrictedField: 'restricted' }); + + await page.goto(url.edit(id)); + + await expect(page.locator('#field-group__restrictedGroupText')).toHaveCount(0); + }); + + test('field without read access inside a collapsible should not show', async () => { + const { id } = await createDoc({ restrictedField: 'restricted' }); + + await page.goto(url.edit(id)); + + await expect(page.locator('#field-restrictedRowText')).toHaveCount(0); + }); + + test('field without read access inside a row should not show', async () => { + const { id } = await createDoc({ restrictedField: 'restricted' }); + + await page.goto(url.edit(id)); + + await expect(page.locator('#field-restrictedCollapsibleText')).toHaveCount(0); }); describe('restricted collection', () => { diff --git a/test/admin/payload-types.ts b/test/admin/payload-types.ts index b1e2c80c45..fa4d331688 100644 --- a/test/admin/payload-types.ts +++ b/test/admin/payload-types.ts @@ -16,14 +16,19 @@ export interface Global { } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "posts". + * via the `definition` "group-globals-one". */ -export interface Post { +export interface GroupGlobalsOne { + id: string; + title?: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-globals-two". + */ +export interface GroupGlobalsTwo { id: string; title?: string; - description?: string; - createdAt: string; - updatedAt: string; } /** * This interface was referenced by `Config`'s JSON-Schema @@ -39,3 +44,55 @@ export interface User { createdAt: string; updatedAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts". + */ +export interface Post { + id: string; + title?: string; + description?: string; + number?: number; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-one-collection-ones". + */ +export interface GroupOneCollectionOne { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-one-collection-twos". + */ +export interface GroupOneCollectionTwo { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-two-collection-ones". + */ +export interface GroupTwoCollectionOne { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "group-two-collection-twos". + */ +export interface GroupTwoCollectionTwo { + id: string; + title?: string; + createdAt: string; + updatedAt: string; +} diff --git a/test/endpoints/config.ts b/test/endpoints/config.ts index 171a3690c2..4d105cc7e1 100644 --- a/test/endpoints/config.ts +++ b/test/endpoints/config.ts @@ -1,16 +1,18 @@ -import { Response } from 'express'; +import express, { Response } from 'express'; import { devUser } from '../credentials'; import { buildConfig } from '../buildConfig'; import { openAccess } from '../helpers/configHelpers'; import { PayloadRequest } from '../../src/express/types'; +import { Config } from '../../src/config/types'; export const collectionSlug = 'endpoints'; export const globalSlug = 'global-endpoints'; export const globalEndpoint = 'global'; export const applicationEndpoint = 'path'; +export const rootEndpoint = 'root'; -export default buildConfig({ +const MyConfig: Config = { collections: [ { slug: collectionSlug, @@ -77,6 +79,32 @@ export default buildConfig({ res.json(req.body); }, }, + { + path: `/${applicationEndpoint}`, + method: 'get', + handler: (req: PayloadRequest, res: Response): void => { + res.json({ message: 'Hello, world!' }); + }, + }, + { + path: `/${rootEndpoint}`, + method: 'get', + root: true, + handler: (req: PayloadRequest, res: Response): void => { + res.json({ message: 'Hello, world!' }); + }, + }, + { + path: `/${rootEndpoint}`, + method: 'post', + root: true, + handler: [ + express.json({ type: 'application/json' }), + (req: PayloadRequest, res: Response): void => { + res.json(req.body); + } + ], + }, ], onInit: async (payload) => { await payload.create({ @@ -87,4 +115,6 @@ export default buildConfig({ }, }); }, -}); +} + +export default buildConfig(MyConfig); diff --git a/test/endpoints/int.spec.ts b/test/endpoints/int.spec.ts index 404f1dcf49..9844e852a5 100644 --- a/test/endpoints/int.spec.ts +++ b/test/endpoints/int.spec.ts @@ -1,6 +1,6 @@ import { initPayloadTest } from '../helpers/configHelpers'; import { RESTClient } from '../helpers/rest'; -import { applicationEndpoint, collectionSlug, globalEndpoint, globalSlug } from './config'; +import { applicationEndpoint, collectionSlug, globalEndpoint, globalSlug, rootEndpoint } from './config'; require('isomorphic-fetch'); @@ -15,21 +15,21 @@ describe('Endpoints', () => { describe('Collections', () => { it('should GET a static endpoint', async () => { - const { status, data } = await client.endpoint(`/${collectionSlug}/say-hello/joe-bloggs`); + const { status, data } = await client.endpoint(`/api/${collectionSlug}/say-hello/joe-bloggs`); expect(status).toBe(200); expect(data.message).toStrictEqual('Hey Joey!'); }); it('should GET an endpoint with a parameter', async () => { const name = 'George'; - const { status, data } = await client.endpoint(`/${collectionSlug}/say-hello/${name}`); + const { status, data } = await client.endpoint(`/api/${collectionSlug}/say-hello/${name}`); expect(status).toBe(200); expect(data.message).toStrictEqual(`Hello ${name}!`); }); it('should POST an endpoint with data', async () => { const params = { name: 'George', age: 29 }; - const { status, data } = await client.endpoint(`/${collectionSlug}/whoami`, 'post', params); + const { status, data } = await client.endpoint(`/api/${collectionSlug}/whoami`, 'post', params); expect(status).toBe(200); expect(data.name).toStrictEqual(params.name); expect(data.age).toStrictEqual(params.age); @@ -39,7 +39,7 @@ describe('Endpoints', () => { describe('Globals', () => { it('should call custom endpoint', async () => { const params = { globals: 'response' }; - const { status, data } = await client.endpoint(`/globals/${globalSlug}/${globalEndpoint}`, 'post', params); + const { status, data } = await client.endpoint(`/api/globals/${globalSlug}/${globalEndpoint}`, 'post', params); expect(status).toBe(200); expect(params).toMatchObject(data); @@ -49,7 +49,17 @@ describe('Endpoints', () => { describe('API', () => { it('should call custom endpoint', async () => { const params = { app: 'response' }; - const { status, data } = await client.endpoint(`/${applicationEndpoint}`, 'post', params); + const { status, data } = await client.endpoint(`/api/${applicationEndpoint}`, 'post', params); + + expect(status).toBe(200); + expect(params).toMatchObject(data); + }); + }); + + describe('Root', () => { + it('should call custom root endpoint', async () => { + const params = { root: 'response' }; + const { status, data } = await client.endpoint(`/${rootEndpoint}`, 'post', params); expect(status).toBe(200); expect(params).toMatchObject(data); diff --git a/test/fields-relationship/e2e.spec.ts b/test/fields-relationship/e2e.spec.ts index 8c096a55ba..79aa38fd9d 100644 --- a/test/fields-relationship/e2e.spec.ts +++ b/test/fields-relationship/e2e.spec.ts @@ -169,6 +169,16 @@ describe('fields - relationship', () => { await saveDocAndAssert(page); }); + test('should duplicate document with relationships', async () => { + await page.goto(url.edit(docWithExistingRelations.id)); + + await page.locator('.btn.duplicate').first().click(); + await expect(page.locator('.Toastify')).toContainText('successfully'); + const field = page.locator('#field-relationship .rs__value-container'); + + await expect(field).toHaveText(relationOneDoc.id); + }); + describe('existing relationships', () => { test('should highlight existing relationship', async () => { await page.goto(url.edit(docWithExistingRelations.id)); diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index a10e766fe8..8bba442033 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -16,6 +16,10 @@ export interface ArrayField { text: string; id?: string; }[]; + collapsedArray: { + text: string; + id?: string; + }[]; localized: { text: string; id?: string; @@ -80,6 +84,49 @@ export interface BlockField { blockType: 'tabs'; } )[]; + collapsedByDefaultBlocks: ( + | { + text: string; + richText?: { + [k: string]: unknown; + }[]; + id?: string; + blockName?: string; + blockType: 'text'; + } + | { + number: number; + id?: string; + blockName?: string; + blockType: 'number'; + } + | { + subBlocks: ( + | { + text: string; + id?: string; + blockName?: string; + blockType: 'text'; + } + | { + number: number; + id?: string; + blockName?: string; + blockType: 'number'; + } + )[]; + id?: string; + blockName?: string; + blockType: 'subBlocks'; + } + | { + textInCollapsible?: string; + textInRow?: string; + id?: string; + blockName?: string; + blockType: 'tabs'; + } + )[]; localizedBlocks: ( | { text: string; @@ -153,6 +200,7 @@ export interface CollapsibleField { textWithinSubGroup?: string; }; }; + someText?: string; createdAt: string; updatedAt: string; } diff --git a/test/helpers/rest.ts b/test/helpers/rest.ts index 3188a4b27c..59b0358568 100644 --- a/test/helpers/rest.ts +++ b/test/helpers/rest.ts @@ -255,8 +255,8 @@ export class RESTClient { return { status, doc: result }; } - async endpoint(path: string, method = 'get', params = undefined): Promise<{status: number, data: T}> { - const response = await fetch(`${this.serverURL}/api${path}`, { + async endpoint(path: string, method = 'get', params = undefined): Promise<{ status: number, data: T }> { + const response = await fetch(`${this.serverURL}${path}`, { headers: { 'Content-Type': 'application/json', }, diff --git a/yarn.lock b/yarn.lock index a42a2dd62c..8c04dfb094 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1046,15 +1046,6 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" -"@bahmutov/cy-api@^2.1.3": - version "2.1.3" - resolved "https://registry.npmjs.org/@bahmutov/cy-api/-/cy-api-2.1.3.tgz#739fc5923629a62e095f95edc3d39ffabd4bf0b7" - integrity sha512-1Djwka3jg9eIE/eVwc1geSBvFLB8dy631A4C9Mwgz4MA89YaLgLz8zr2elTfsylr/3OH/1Uj21bWOp9atRUY7g== - dependencies: - "@types/common-tags" "1.8.1" - common-tags "1.8.2" - highlight.js "11.4.0" - "@bcherny/json-schema-ref-parser@9.0.9": version "9.0.9" resolved "https://registry.npmjs.org/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#09899d405bc708c0acac0066ae8db5b94d465ca4" @@ -1951,11 +1942,6 @@ "@types/node" "*" source-map "^0.6.0" -"@types/common-tags@1.8.1": - version "1.8.1" - resolved "https://registry.npmjs.org/@types/common-tags/-/common-tags-1.8.1.tgz#a5a49ca5ebbb58e0f8947f3ec98950c8970a68a9" - integrity sha512-20R/mDpKSPWdJs5TOpz3e7zqbeCNuMCPhV7Yndk9KU2Rbij2r5W4RzwDPkzC+2lzUqXYu9rFzTktCBnDjHuNQg== - "@types/compression@^1.7.0": version "1.7.2" resolved "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz#7cc1cdb01b4730eea284615a68fc70a2cdfd5e71" @@ -4114,11 +4100,6 @@ commander@^8.3.0: resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -common-tags@1.8.2: - version "1.8.2" - resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -6595,11 +6576,6 @@ he@^1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -highlight.js@11.4.0: - version "11.4.0" - resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.4.0.tgz#34ceadd49e1596ee5aba3d99346cdfd4845ee05a" - integrity sha512-nawlpCBCSASs7EdvZOYOYVkJpGmAOKMYZgZtUqSRqodZE0GRVcFKwo1RcpeOemqh9hyttTdd5wDBwHkuSyUfnA== - history@^4.9.0: version "4.10.1" resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"