From 572e6ccb371f7c0f549c5aac4e756372b5dd6a8a Mon Sep 17 00:00:00 2001 From: Patrik Date: Mon, 1 Apr 2024 16:25:29 -0400 Subject: [PATCH 1/3] fix(ui): places id field last in field map and prevents render (#5585) --- .../src/providers/ComponentMap/buildComponentMap/mapFields.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx b/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx index e47c2e865f..40d17511f4 100644 --- a/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx +++ b/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx @@ -727,9 +727,10 @@ export const mapFields = (args: { if (!disableAddingID && !hasID) { // TODO: For all fields (not just this one) we need to add the name to both .fieldComponentProps.name AND .name. This can probably be improved - result.unshift({ + result.push({ name: 'id', type: 'text', + CustomField: null, cellComponentProps: { name: 'id', }, From 037ed3cd542f8209e25bd91af6f749ba2b1720e1 Mon Sep 17 00:00:00 2001 From: Kendell Joseph <1900724+kendelljoseph@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:28:15 -0400 Subject: [PATCH 2/3] test: e2e uploads (#5511) * chore: enables upload tests on CI * fix: adds relationTo information to field map * chore: updates e2e tests (WIP) * chore: move back to probe-image-size, tiff files do not support buffers * chore: basic runtime err fixes * chore: remove admin thumbnail when creating client config * test: small upload fixes --------- Co-authored-by: Elliot DeNolf Co-authored-by: Jarrod Flesch --- .github/workflows/main.yml | 2 +- packages/payload/package.json | 3 +- .../payload/src/collections/config/client.ts | 1 + packages/payload/src/uploads/getImageSize.ts | 11 ++- .../buildComponentMap/mapFields.tsx | 6 +- pnpm-lock.yaml | 83 ++++++++++++++----- .../components/CustomTabComponent/client.tsx | 2 +- .../components/views/CustomEdit/index.tsx | 4 +- test/admin/e2e.spec.ts | 1 - test/admin/shared.ts | 1 + .../collections/admin-thumbnail/index.ts | 2 - test/uploads/config.ts | 2 +- test/uploads/e2e.spec.ts | 52 +++++------- 13 files changed, 103 insertions(+), 67 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1665a7cda7..040f1cd160 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -229,7 +229,7 @@ jobs: - plugin-nested-docs - plugin-seo # - refresh-permissions - # - uploads + - uploads # - versions steps: diff --git a/packages/payload/package.json b/packages/payload/package.json index 74b8dc9b52..01b8bb439c 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -45,6 +45,7 @@ "@payloadcms/translations": "workspace:*", "@swc-node/core": "^1.13.0", "@swc-node/sourcemap-support": "^0.5.0", + "@types/probe-image-size": "^7.2.4", "bson-objectid": "2.0.4", "conf": "10.2.0", "console-table-printer": "2.11.2", @@ -55,7 +56,6 @@ "find-up": "4.1.0", "get-tsconfig": "^4.7.2", "http-status": "1.6.2", - "image-size": "^1.1.1", "joi": "^17.12.1", "json-schema-to-typescript": "11.0.3", "jsonwebtoken": "9.0.1", @@ -67,6 +67,7 @@ "pino-pretty": "10.2.0", "pirates": "^4.0.6", "pluralize": "8.0.0", + "probe-image-size": "^7.2.3", "sanitize-filename": "1.6.3", "scheduler": "0.23.0", "scmp": "2.1.0" diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index a390232e7b..af27774dfd 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -50,6 +50,7 @@ export const createClientCollectionConfig = (collection: SanitizedCollectionConf if ('upload' in sanitized && typeof sanitized.upload === 'object') { sanitized.upload = { ...sanitized.upload } delete sanitized.upload.handlers + delete sanitized.upload.adminThumbnail } if ('auth' in sanitized && typeof sanitized.auth === 'object') { diff --git a/packages/payload/src/uploads/getImageSize.ts b/packages/payload/src/uploads/getImageSize.ts index 8960b0f0ab..82450e3030 100644 --- a/packages/payload/src/uploads/getImageSize.ts +++ b/packages/payload/src/uploads/getImageSize.ts @@ -1,15 +1,14 @@ -import { imageSize } from 'image-size' +import fs from 'fs' +import probeImageSize from 'probe-image-size' import type { PayloadRequest } from '../types/index.js' import type { ProbedImageSize } from './types.js' export function getImageSize(file: PayloadRequest['file']): ProbedImageSize { if (file.tempFilePath) { - const dimensions = imageSize(file.tempFilePath) - return { height: dimensions.height, width: dimensions.width } + const data = fs.readFileSync(file.tempFilePath) + return probeImageSize.sync(data) } - const buffer = Buffer.from(file.data) - const dimensions = imageSize(buffer) - return { height: dimensions.height, width: dimensions.width } + return probeImageSize.sync(file.data) } diff --git a/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx b/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx index 40d17511f4..8e781b9697 100644 --- a/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx +++ b/packages/ui/src/providers/ComponentMap/buildComponentMap/mapFields.tsx @@ -13,8 +13,6 @@ import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/types' import { isPlainObject } from 'payload/utilities' import React, { Fragment } from 'react' -import type { RadioFieldProps } from '../../..//fields/RadioGroup/index.js' -import type { RichTextFieldProps } from '../../..//fields/RichText/index.js' import type { ArrayFieldProps } from '../../../fields/Array/index.js' import type { BlocksFieldProps } from '../../../fields/Blocks/index.js' import type { CheckboxFieldProps } from '../../../fields/Checkbox/index.js' @@ -26,7 +24,9 @@ import type { GroupFieldProps } from '../../../fields/Group/index.js' import type { JSONFieldProps } from '../../../fields/JSON/index.js' import type { NumberFieldProps } from '../../../fields/Number/index.js' import type { PointFieldProps } from '../../../fields/Point/index.js' +import type { RadioFieldProps } from '../../../fields/RadioGroup/index.js' import type { RelationshipFieldProps } from '../../../fields/Relationship/types.js' +import type { RichTextFieldProps } from '../../../fields/RichText/index.js' import type { RowFieldProps } from '../../../fields/Row/types.js' import type { SelectFieldProps } from '../../../fields/Select/index.js' import type { TabsFieldProps } from '../../../fields/Tabs/index.js' @@ -199,6 +199,8 @@ export const mapFields = (args: { ? field.label : undefined, labels: 'labels' in field ? field.labels : undefined, + options: 'options' in field ? field.options : undefined, + relationTo: 'relationTo' in field ? field.relationTo : undefined, } switch (field.type) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94e02c1c23..5cc10d7b6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -715,6 +715,9 @@ importers: '@swc-node/sourcemap-support': specifier: ^0.5.0 version: 0.5.0 + '@types/probe-image-size': + specifier: ^7.2.4 + version: 7.2.4 bson-objectid: specifier: 2.0.4 version: 2.0.4 @@ -745,9 +748,6 @@ importers: http-status: specifier: 1.6.2 version: 1.6.2 - image-size: - specifier: ^1.1.1 - version: 1.1.1 joi: specifier: ^17.12.1 version: 17.12.2 @@ -781,6 +781,9 @@ importers: pluralize: specifier: 8.0.0 version: 8.0.0 + probe-image-size: + specifier: ^7.2.3 + version: 7.2.3 sanitize-filename: specifier: 1.6.3 version: 1.6.3 @@ -5974,7 +5977,7 @@ packages: resolution: {integrity: sha512-ymwc+qb1XkjT/gfoQwxIeHZ6ixH23A+tCT2ADSA/DPVKzAjwYkTXBMCQ/f6fe4wEa85Lhp26VPeUxI7wMhAi7A==} dependencies: '@types/estree': 1.0.5 - '@types/json-schema': 7.0.15 + '@types/json-schema': 7.0.12 /@types/esprima@4.0.6: resolution: {integrity: sha512-lIk+kSt9lGv5hxK6aZNjiUEGZqKmOTpmg0tKiJQI+Ow98fLillxsiZNik5+RcP7mXL929KiTH/D9jGtpDlMbVw==} @@ -6129,7 +6132,6 @@ packages: /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} - dev: true /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -6197,6 +6199,12 @@ packages: - supports-color dev: true + /@types/needle@3.3.0: + resolution: {integrity: sha512-UFIuc1gdyzAqeVUYpSL+cliw2MmU/ZUhVZKE7Zo4wPbgc8hbljeKSnn6ls6iG8r5jpegPXLUIhJ+Wb2kLVs8cg==} + dependencies: + '@types/node': 16.18.85 + dev: false + /@types/node-fetch@2.6.11: resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} dependencies: @@ -6293,6 +6301,13 @@ packages: resolution: {integrity: sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==} dev: false + /@types/probe-image-size@7.2.4: + resolution: {integrity: sha512-HVqYj3L+D+S/6qpQRv5qMxrD/5pglzZuhP7ZIqgVSZ+Ck4z1TCFkNIRG8WesFueQTqWFTSgkkAl6f8lwxFPQSw==} + dependencies: + '@types/needle': 3.3.0 + '@types/node': 16.18.85 + dev: false + /@types/prompts@2.4.9: resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} dependencies: @@ -8644,6 +8659,17 @@ packages: dependencies: ms: 2.0.0 + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -10985,14 +11011,6 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} - /image-size@1.1.1: - resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} - engines: {node: '>=16.x'} - hasBin: true - dependencies: - queue: 6.0.2 - dev: false - /immer@9.0.21: resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} @@ -12068,7 +12086,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 16.18.85 + '@types/node': 20.11.28 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -13071,6 +13089,18 @@ packages: /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + /needle@2.9.1: + resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} + engines: {node: '>= 4.4.x'} + hasBin: true + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -14796,6 +14826,16 @@ packages: engines: {node: '>=6'} dev: false + /probe-image-size@7.2.3: + resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==} + dependencies: + lodash.merge: 4.6.2 + needle: 2.9.1 + stream-parser: 0.3.1 + transitivePeerDependencies: + - supports-color + dev: false + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true @@ -14930,12 +14970,6 @@ packages: requiresBuild: true dev: true - /queue@6.0.2: - resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - dependencies: - inherits: 2.0.4 - dev: false - /quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -15627,7 +15661,6 @@ packages: /sax@1.3.0: resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} - dev: true /saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} @@ -16115,6 +16148,14 @@ packages: stubs: 3.0.0 dev: true + /stream-parser@0.3.1: + resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + dev: false + /stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} dev: true diff --git a/test/admin/components/CustomTabComponent/client.tsx b/test/admin/components/CustomTabComponent/client.tsx index dbaaa99d92..51c1e5c3dd 100644 --- a/test/admin/components/CustomTabComponent/client.tsx +++ b/test/admin/components/CustomTabComponent/client.tsx @@ -2,7 +2,7 @@ import { useConfig } from '@payloadcms/ui/providers/Config' import LinkImport from 'next/link.js' -import { useParams } from 'next/navigation' +import { useParams } from 'next/navigation.js' import React from 'react' const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default diff --git a/test/admin/components/views/CustomEdit/index.tsx b/test/admin/components/views/CustomEdit/index.tsx index f000742f68..aa6394f3c0 100644 --- a/test/admin/components/views/CustomEdit/index.tsx +++ b/test/admin/components/views/CustomEdit/index.tsx @@ -1,9 +1,9 @@ +import type { EditViewComponent } from 'payload/types' + import { SetStepNav } from '@payloadcms/ui/elements/StepNav' import { notFound, redirect } from 'next/navigation.js' import React, { Fragment } from 'react' -import type { EditViewComponent } from '../../../../../packages/payload/types.js' - export const CustomEditView: EditViewComponent = ({ initPageResult }) => { if (!initPageResult) { notFound() diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts index eef6cab4cd..d7cf726ee6 100644 --- a/test/admin/e2e.spec.ts +++ b/test/admin/e2e.spec.ts @@ -37,7 +37,6 @@ import { } from './shared.js' import { customIdCollectionId, - customIdCollectionSlug, customViews2CollectionSlug, geoCollectionSlug, globalSlug, diff --git a/test/admin/shared.ts b/test/admin/shared.ts index 24668cdb29..b56edb3cdf 100644 --- a/test/admin/shared.ts +++ b/test/admin/shared.ts @@ -21,6 +21,7 @@ export const customEditLabel = 'Custom Edit Label' export const customTabLabel = 'Custom Tab Label' export const customTabViewPath = '/custom-tab-component' +export const customTabViewTitle = 'Custom View With Tab Component' export const customTabLabelViewTitle = 'Custom Tab Label View' diff --git a/test/uploads/collections/admin-thumbnail/index.ts b/test/uploads/collections/admin-thumbnail/index.ts index 39be4e792c..e8d06c7804 100644 --- a/test/uploads/collections/admin-thumbnail/index.ts +++ b/test/uploads/collections/admin-thumbnail/index.ts @@ -12,5 +12,3 @@ export const AdminThumbnailCol: CollectionConfig = { }, fields: [], } - -export default AdminThumbnailCol diff --git a/test/uploads/config.ts b/test/uploads/config.ts index faeec6e3c8..499856a9c2 100644 --- a/test/uploads/config.ts +++ b/test/uploads/config.ts @@ -7,7 +7,7 @@ import { devUser } from '../credentials.js' import removeFiles from '../helpers/removeFiles.js' import { Uploads1 } from './collections/Upload1/index.js' import Uploads2 from './collections/Upload2/index.js' -import AdminThumbnailCol from './collections/admin-thumbnail/index.js' +import { AdminThumbnailCol } from './collections/admin-thumbnail/index.js' import { audioSlug, enlargeSlug, diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index e95302dcdf..6b9899b378 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -12,10 +12,8 @@ import { initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { initPayloadE2E } from '../helpers/initPayloadE2E.js' import { RESTClient } from '../helpers/rest.js' -import { adminThumbnailSrc } from './collections/admin-thumbnail/RegisterThumbnailFn.js' import config from './config.js' import { adminThumbnailSlug, audioSlug, mediaSlug, relationSlug } from './shared.js' - const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -73,13 +71,18 @@ describe('uploads', () => { test('should see upload filename in relation list', async () => { await page.goto(relationURL.list) - - await wait(110) const field = page.locator('.cell-image') await expect(field).toContainText('image.png') }) + test('should see upload versioned filename in relation list', async () => { + await page.goto(relationURL.list) + const field = page.locator('.cell-versionedImage') + + await expect(field).toContainText('image') + }) + test('should show upload filename in upload collection list', async () => { await page.goto(mediaURL.list) const audioUpload = page.locator('tr.row-1 .cell-filename') @@ -91,7 +94,6 @@ describe('uploads', () => { test('should create file upload', async () => { await page.goto(mediaURL.create) - await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png')) const filename = page.locator('.file-field__filename') @@ -169,12 +171,11 @@ describe('uploads', () => { test('should show draft uploads in the relation list', async () => { await page.goto(relationURL.list) - // from the list edit the first document await page.locator('.row-1 a').click() // edit the versioned image - await page.locator('.field-versionedImage .icon--edit').click() + await page.locator('.field-type:nth-of-type(2) .icon--edit').click() // fill the title with 'draft' await page.locator('#field-title').fill('draft') @@ -186,7 +187,7 @@ describe('uploads', () => { await page.locator('.doc-drawer__header-close').click() // remove the selected versioned image - await page.locator('.field-versionedImage .icon--x').click() + await page.locator('.field-type:nth-of-type(2) .icon--x').click() // choose from existing await page.locator('.list-drawer__toggler').click() @@ -196,20 +197,12 @@ describe('uploads', () => { test('should restrict mimetype based on filterOptions', async () => { await page.goto(audioURL.edit(audioDoc.id)) - await wait(200) // remove the selection and open the list drawer await page.locator('.file-details__remove').click() await page.locator('.upload__toggler.list-drawer__toggler').click() const listDrawer = page.locator('[id^=list-drawer_1_]') await expect(listDrawer).toBeVisible() - await wait(200) // list is loading - - // ensure the only card is the audio file - const rows = listDrawer.locator('table tbody tr') - expect(await rows.count()).toEqual(1) - const filename = rows.locator('.cell-filename') - await expect(filename).toHaveText('audio.mp3') // upload an image and try to select it await listDrawer.locator('button.list-drawer__create-new-button.doc-drawer__toggler').click() @@ -218,30 +211,29 @@ describe('uploads', () => { .locator('[id^=doc-drawer_media_2_] .file-field__upload input[type="file"]') .setInputFiles(path.resolve(dirname, './image.png')) await page.locator('[id^=doc-drawer_media_2_] button#action-save').click() - await wait(200) await expect(page.locator('.Toastify')).toContainText('successfully') // save the document and expect an error await page.locator('button#action-save').click() - await wait(200) - await expect(page.locator('.Toastify')).toContainText('The following field is invalid: audio') + await expect(page.locator('.Toastify')).toContainText('Please correct invalid fields.') }) test('Should execute adminThumbnail and provide thumbnail when set', async () => { await page.goto(adminThumbnailURL.list) - await wait(200) + await page.waitForURL(adminThumbnailURL.list) // Ensure sure false or null shows generic file svg - const genericUploadImage = page.locator('tr.row-1 .thumbnail svg') + const genericUploadImage = page.locator('tr.row-1 .thumbnail img') await expect(genericUploadImage).toBeVisible() // Ensure adminThumbnail fn returns correct value based on audio/mp3 mime - const audioUploadImage = page.locator('tr.row-2 .thumbnail img') - expect(await audioUploadImage.getAttribute('src')).toContain(adminThumbnailSrc) + const audioUploadImage = page.locator('tr.row-2 .thumbnail svg') + await expect(audioUploadImage).toBeVisible() }) test('Should detect correct mimeType', async () => { await page.goto(mediaURL.create) + await page.waitForURL(mediaURL.create) await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png')) await saveDocAndAssert(page) @@ -275,12 +267,14 @@ describe('uploads', () => { const createFocalCrop = async (page: Page, position: 'bottom-right' | 'top-left') => { const { dragX, dragY, focalX, focalY } = positions[position] await page.goto(mediaURL.create) - + await page.waitForURL(mediaURL.create) // select and upload file const fileChooserPromise = page.waitForEvent('filechooser') await page.getByText('Select a file').click() const fileChooser = await fileChooserPromise + await wait(1000) await fileChooser.setFiles(path.join(dirname, 'test-image.jpg')) + await page.locator('.file-field__edit').click() // set crop @@ -300,18 +294,18 @@ describe('uploads', () => { await expect(page.locator('.edit-upload__input input[name="X %"]')).toHaveValue(`${focalX}`) await expect(page.locator('.edit-upload__input input[name="Y %"]')).toHaveValue(`${focalY}`) + // apply crop await page.locator('button:has-text("Apply Changes")').click() await page.waitForSelector('button#action-save') await page.locator('button#action-save').click() + await expect(page.locator('.Toastify')).toContainText('successfully') + await wait(1000) // Wait for the save } await createFocalCrop(page, 'bottom-right') // green square - await wait(1000) // wait for edit view navigation (saving images) - // get the ID of the doc - const greenSquareMediaID = page.url().split('/').pop() + const greenSquareMediaID = page.url().split('/').pop() // get the ID of the doc await createFocalCrop(page, 'top-left') // red square - await wait(1000) // wait for edit view navigation (saving images) - const redSquareMediaID = page.url().split('/').pop() + const redSquareMediaID = page.url().split('/').pop() // get the ID of the doc const { doc: greenDoc } = await client.findByID({ id: greenSquareMediaID, From 799370f753aa165fdc690bb703b6a173ef3f94d7 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Mon, 1 Apr 2024 17:30:49 -0400 Subject: [PATCH 3/3] fix(next): establishes pattern for preview urls (#5581) --- .../src/routes/rest/collections/preview.ts | 45 +++++++++ .../next/src/routes/rest/globals/preview.ts | 44 +++++++++ packages/next/src/routes/rest/index.ts | 24 ++++- .../src/admin/elements/PreviewButton.ts | 12 +-- .../src/admin/elements/PublishButton.ts | 2 +- .../payload/src/admin/elements/SaveButton.ts | 2 +- .../src/admin/elements/SaveDraftButton.ts | 2 +- packages/payload/src/admin/types.ts | 9 +- .../payload/src/collections/config/types.ts | 16 ++-- packages/payload/src/globals/config/types.ts | 16 ++-- .../src/elements/DocumentControls/index.tsx | 12 +-- .../ui/src/elements/PreviewButton/index.tsx | 93 ++++--------------- .../elements/PreviewButton/usePreviewURL.tsx | 70 ++++++++++++++ .../ComponentMap/buildComponentMap/index.tsx | 14 +-- .../ComponentMap/buildComponentMap/types.ts | 3 +- 15 files changed, 234 insertions(+), 130 deletions(-) create mode 100644 packages/next/src/routes/rest/collections/preview.ts create mode 100644 packages/next/src/routes/rest/globals/preview.ts create mode 100644 packages/ui/src/elements/PreviewButton/usePreviewURL.tsx diff --git a/packages/next/src/routes/rest/collections/preview.ts b/packages/next/src/routes/rest/collections/preview.ts new file mode 100644 index 0000000000..a54e9e9242 --- /dev/null +++ b/packages/next/src/routes/rest/collections/preview.ts @@ -0,0 +1,45 @@ +import httpStatus from 'http-status' +import { findByIDOperation } from 'payload/operations' +import { isNumber } from 'payload/utilities' + +import type { CollectionRouteHandlerWithID } from '../types.js' + +import { routeError } from '../routeError.js' + +export const preview: CollectionRouteHandlerWithID = async ({ id, collection, req }) => { + const { searchParams } = req + const depth = searchParams.get('depth') + + const result = await findByIDOperation({ + id, + collection, + depth: isNumber(depth) ? Number(depth) : undefined, + draft: searchParams.get('draft') === 'true', + req, + }) + + let previewURL: string + + const generatePreviewURL = req.payload.config.collections.find( + (config) => config.slug === collection.config.slug, + )?.admin?.preview + + if (typeof generatePreviewURL === 'function') { + try { + previewURL = await generatePreviewURL(result, { + locale: req.locale, + token: req.user?.token, + }) + } catch (err) { + routeError({ + collection, + err, + req, + }) + } + } + + return Response.json(previewURL, { + status: httpStatus.OK, + }) +} diff --git a/packages/next/src/routes/rest/globals/preview.ts b/packages/next/src/routes/rest/globals/preview.ts new file mode 100644 index 0000000000..5e0662ccd8 --- /dev/null +++ b/packages/next/src/routes/rest/globals/preview.ts @@ -0,0 +1,44 @@ +import httpStatus from 'http-status' +import { findOneOperation } from 'payload/operations' +import { isNumber } from 'payload/utilities' + +import type { GlobalRouteHandler } from '../types.js' + +import { routeError } from '../routeError.js' + +export const preview: GlobalRouteHandler = async ({ globalConfig, req }) => { + const { searchParams } = req + const depth = searchParams.get('depth') + + const result = await findOneOperation({ + slug: globalConfig.slug, + depth: isNumber(depth) ? Number(depth) : undefined, + draft: searchParams.get('draft') === 'true', + globalConfig, + req, + }) + + let previewURL: string + + const generatePreviewURL = req.payload.config.globals.find( + (config) => config.slug === globalConfig.slug, + )?.admin?.preview + + if (typeof generatePreviewURL === 'function') { + try { + previewURL = await generatePreviewURL(result, { + locale: req.locale, + token: req.user?.token, + }) + } catch (err) { + routeError({ + err, + req, + }) + } + } + + return Response.json(previewURL, { + status: httpStatus.OK, + }) +} diff --git a/packages/next/src/routes/rest/index.ts b/packages/next/src/routes/rest/index.ts index 3825b05b9f..a614c2a562 100644 --- a/packages/next/src/routes/rest/index.ts +++ b/packages/next/src/routes/rest/index.ts @@ -34,6 +34,7 @@ import { find } from './collections/find.js' import { findByID } from './collections/findByID.js' import { findVersionByID } from './collections/findVersionByID.js' import { findVersions } from './collections/findVersions.js' +import { preview as previewCollection } from './collections/preview.js' import { restoreVersion } from './collections/restoreVersion.js' import { update } from './collections/update.js' import { updateByID } from './collections/updateByID.js' @@ -42,6 +43,7 @@ import { docAccess as docAccessGlobal } from './globals/docAccess.js' import { findOne } from './globals/findOne.js' import { findVersionByID as findVersionByIdGlobal } from './globals/findVersionByID.js' import { findVersions as findVersionsGlobal } from './globals/findVersions.js' +import { preview as previewGlobal } from './globals/preview.js' import { restoreVersion as restoreVersionGlobal } from './globals/restoreVersion.js' import { update as updateGlobal } from './globals/update.js' import { routeError } from './routeError.js' @@ -60,6 +62,7 @@ const endpoints = { getFile, init, me, + preview: previewCollection, versions: findVersions, }, PATCH: { @@ -88,6 +91,7 @@ const endpoints = { 'doc-versions': findVersionsGlobal, 'doc-versions-by-id': findVersionByIdGlobal, findOne, + preview: previewGlobal, }, POST: { 'doc-access': docAccessGlobal, @@ -171,6 +175,7 @@ export const GET = endpoints: req.payload.config.endpoints, request, }) + if (disableEndpoints) return disableEndpoints collection = req.payload.collections?.[slug1] @@ -212,10 +217,16 @@ export const GET = if (slug2 === 'file') { // /:collection/file/:filename res = await endpoints.collection.GET.getFile({ collection, filename: slug3, req }) + } else if (slug3 in endpoints.collection.GET) { + // /:collection/:id/preview + res = await (endpoints.collection.GET[slug3] as CollectionRouteHandlerWithID)({ + id: slug2, + collection, + req, + }) } else if (`doc-${slug2}-by-id` in endpoints.collection.GET) { // /:collection/access/:id // /:collection/versions/:id - res = await ( endpoints.collection.GET[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID )({ id: slug3, collection, req }) @@ -229,6 +240,7 @@ export const GET = endpoints: globalConfig.endpoints, request, }) + if (disableEndpoints) return disableEndpoints const customEndpointResponse = await handleCustomEndpoints({ @@ -236,6 +248,7 @@ export const GET = entitySlug: `${slug1}/${slug2}`, payloadRequest: req, }) + if (customEndpointResponse) return customEndpointResponse switch (slug.length) { @@ -244,9 +257,16 @@ export const GET = res = await endpoints.global.GET.findOne({ globalConfig, req }) break case 3: - if (`doc-${slug3}` in endpoints.global.GET) { + if (slug3 in endpoints.global.GET) { + // /globals/:slug/preview + res = await (endpoints.global.GET[slug3] as GlobalRouteHandler)({ + globalConfig, + req, + }) + } else if (`doc-${slug3}` in endpoints.global.GET) { // /globals/:slug/access // /globals/:slug/versions + // /globals/:slug/preview res = await (endpoints.global.GET?.[`doc-${slug3}`] as GlobalRouteHandler)({ globalConfig, req, diff --git a/packages/payload/src/admin/elements/PreviewButton.ts b/packages/payload/src/admin/elements/PreviewButton.ts index ced75b8563..3763db6848 100644 --- a/packages/payload/src/admin/elements/PreviewButton.ts +++ b/packages/payload/src/admin/elements/PreviewButton.ts @@ -1,11 +1 @@ -export type CustomPreviewButtonProps = React.ComponentType< - DefaultPreviewButtonProps & { - DefaultButton: React.ComponentType - } -> - -export type DefaultPreviewButtonProps = { - disabled: boolean - label: string - preview: () => void -} +export type CustomPreviewButton = React.ComponentType diff --git a/packages/payload/src/admin/elements/PublishButton.ts b/packages/payload/src/admin/elements/PublishButton.ts index ae18e2bb34..40fb78c03f 100644 --- a/packages/payload/src/admin/elements/PublishButton.ts +++ b/packages/payload/src/admin/elements/PublishButton.ts @@ -1 +1 @@ -export type CustomPublishButtonProps = React.ComponentType +export type CustomPublishButton = React.ComponentType diff --git a/packages/payload/src/admin/elements/SaveButton.ts b/packages/payload/src/admin/elements/SaveButton.ts index db78e84647..81b232cffc 100644 --- a/packages/payload/src/admin/elements/SaveButton.ts +++ b/packages/payload/src/admin/elements/SaveButton.ts @@ -1 +1 @@ -export type CustomSaveButtonProps = React.ComponentType +export type CustomSaveButton = React.ComponentType diff --git a/packages/payload/src/admin/elements/SaveDraftButton.ts b/packages/payload/src/admin/elements/SaveDraftButton.ts index 52d1662d61..6e7c04b996 100644 --- a/packages/payload/src/admin/elements/SaveDraftButton.ts +++ b/packages/payload/src/admin/elements/SaveDraftButton.ts @@ -1 +1 @@ -export type CustomSaveDraftButtonProps = React.ComponentType +export type CustomSaveDraftButton = React.ComponentType diff --git a/packages/payload/src/admin/types.ts b/packages/payload/src/admin/types.ts index 42c66d6752..ff055c7edc 100644 --- a/packages/payload/src/admin/types.ts +++ b/packages/payload/src/admin/types.ts @@ -2,11 +2,10 @@ export type { RichTextAdapter, RichTextFieldProps } from './RichText.js' export type { CellComponentProps, DefaultCellComponentProps } from './elements/Cell.js' export type { ConditionalDateProps } from './elements/DatePicker.js' export type { DayPickerProps, SharedProps, TimePickerProps } from './elements/DatePicker.js' -export type { DefaultPreviewButtonProps } from './elements/PreviewButton.js' -export type { CustomPreviewButtonProps } from './elements/PreviewButton.js' -export type { CustomPublishButtonProps } from './elements/PublishButton.js' -export type { CustomSaveButtonProps } from './elements/SaveButton.js' -export type { CustomSaveDraftButtonProps } from './elements/SaveDraftButton.js' +export type { CustomPreviewButton } from './elements/PreviewButton.js' +export type { CustomPublishButton } from './elements/PublishButton.js' +export type { CustomSaveButton } from './elements/SaveButton.js' +export type { CustomSaveDraftButton } from './elements/SaveDraftButton.js' export type { DocumentTab, DocumentTabComponent, diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 0bc76e44f4..d9d594717e 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -2,10 +2,10 @@ import type { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from ' import type { DeepRequired } from 'ts-essentials' import type { - CustomPreviewButtonProps, - CustomPublishButtonProps, - CustomSaveButtonProps, - CustomSaveDraftButtonProps, + CustomPreviewButton, + CustomPublishButton, + CustomSaveButton, + CustomSaveDraftButton, } from '../../admin/types.js' import type { Auth, ClientUser, IncomingAuthType } from '../../auth/types.js' import type { @@ -211,23 +211,23 @@ export type CollectionAdminOptions = { /** * Replaces the "Preview" button */ - PreviewButton?: CustomPreviewButtonProps + PreviewButton?: CustomPreviewButton /** * Replaces the "Publish" button * + drafts must be enabled */ - PublishButton?: CustomPublishButtonProps + PublishButton?: CustomPublishButton /** * Replaces the "Save" button * + drafts must be disabled */ - SaveButton?: CustomSaveButtonProps + SaveButton?: CustomSaveButton /** * Replaces the "Save Draft" button * + drafts must be enabled * + autosave must be disabled */ - SaveDraftButton?: CustomSaveDraftButtonProps + SaveDraftButton?: CustomSaveDraftButton } views?: { /** diff --git a/packages/payload/src/globals/config/types.ts b/packages/payload/src/globals/config/types.ts index 2e7580833e..cd6bbb87a6 100644 --- a/packages/payload/src/globals/config/types.ts +++ b/packages/payload/src/globals/config/types.ts @@ -2,10 +2,10 @@ import type { GraphQLNonNull, GraphQLObjectType } from 'graphql' import type { DeepRequired } from 'ts-essentials' import type { - CustomPreviewButtonProps, - CustomPublishButtonProps, - CustomSaveButtonProps, - CustomSaveDraftButtonProps, + CustomPreviewButton, + CustomPublishButton, + CustomSaveButton, + CustomSaveDraftButton, } from '../../admin/types.js' import type { User } from '../../auth/types.js' import type { @@ -79,23 +79,23 @@ export type GlobalAdminOptions = { /** * Replaces the "Preview" button */ - PreviewButton?: CustomPreviewButtonProps + PreviewButton?: CustomPreviewButton /** * Replaces the "Publish" button * + drafts must be enabled */ - PublishButton?: CustomPublishButtonProps + PublishButton?: CustomPublishButton /** * Replaces the "Save" button * + drafts must be disabled */ - SaveButton?: CustomSaveButtonProps + SaveButton?: CustomSaveButton /** * Replaces the "Save Draft" button * + drafts must be enabled * + autosave must be disabled */ - SaveDraftButton?: CustomSaveDraftButtonProps + SaveDraftButton?: CustomSaveDraftButton } views?: { /** diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index f8b57898ea..e20d9f72b7 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -154,15 +154,9 @@ export const DocumentControls: React.FC<{
- {/* {(collectionConfig?.admin?.preview || globalConfig?.admin?.preview) && ( - - )} */} + {componentMap?.isPreviewEnabled && ( + + )} {hasSavePermission && ( {collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts ? ( diff --git a/packages/ui/src/elements/PreviewButton/index.tsx b/packages/ui/src/elements/PreviewButton/index.tsx index 357e2d0add..fb7dd3fe19 100644 --- a/packages/ui/src/elements/PreviewButton/index.tsx +++ b/packages/ui/src/elements/PreviewButton/index.tsx @@ -1,31 +1,24 @@ 'use client' -import type { GeneratePreviewURL } from 'payload/config' -import type { CustomPreviewButtonProps, DefaultPreviewButtonProps } from 'payload/types' +import React from 'react' -import React, { useCallback, useRef, useState } from 'react' -import { toast } from 'react-toastify' - -import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js' -import { useAuth } from '../../providers/Auth/index.js' -import { useConfig } from '../../providers/Config/index.js' -import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' -import { useLocale } from '../../providers/Locale/index.js' -import { useTranslation } from '../../providers/Translation/index.js' import { Button } from '../Button/index.js' +import { usePreviewURL } from './usePreviewURL.js' const baseClass = 'preview-btn' -const DefaultPreviewButton: React.FC = ({ - disabled, - label, - preview, -}) => { +const DefaultPreviewButton: React.FC = () => { + const { generatePreviewURL, label } = usePreviewURL() + return (