Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(next, ui): ensures selectAll in the list view ignores locked documents #8813

Merged
merged 2 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/next/src/routes/rest/collections/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'

export const deleteDoc: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, where } = req.query as {
const { depth, overrideLock, where } = req.query as {
depth?: string
overrideLock?: string
where?: Where
}

const result = await deleteOperation({
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
overrideLock: Boolean(overrideLock === 'true'),
req,
where,
})
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/routes/rest/collections/deleteByID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({
}) => {
const { searchParams } = req
const depth = searchParams.get('depth')
const overrideLock = searchParams.get('overrideLock')

const id = sanitizeCollectionID({
id: incomingID,
Expand All @@ -25,6 +26,7 @@ export const deleteByID: CollectionRouteHandlerWithID = async ({
id,
collection,
depth: isNumber(depth) ? depth : undefined,
overrideLock: Boolean(overrideLock === 'true'),
req,
})

Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/routes/rest/collections/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import type { CollectionRouteHandler } from '../types.js'
import { headersWithCors } from '../../../utilities/headersWithCors.js'

export const update: CollectionRouteHandler = async ({ collection, req }) => {
const { depth, draft, limit, where } = req.query as {
const { depth, draft, limit, overrideLock, where } = req.query as {
depth?: string
draft?: string
limit?: string
overrideLock?: string
where?: Where
}

Expand All @@ -23,6 +24,7 @@ export const update: CollectionRouteHandler = async ({ collection, req }) => {
depth: isNumber(depth) ? Number(depth) : undefined,
draft: draft === 'true',
limit: isNumber(limit) ? Number(limit) : undefined,
overrideLock: Boolean(overrideLock === 'true'),
req,
where,
})
Expand Down
2 changes: 2 additions & 0 deletions packages/next/src/routes/rest/collections/updateByID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const updateByID: CollectionRouteHandlerWithID = async ({
const depth = searchParams.get('depth')
const autosave = searchParams.get('autosave') === 'true'
const draft = searchParams.get('draft') === 'true'
const overrideLock = searchParams.get('overrideLock')
const publishSpecificLocale = req.query.publishSpecificLocale as string | undefined

const id = sanitizeCollectionID({
Expand All @@ -31,6 +32,7 @@ export const updateByID: CollectionRouteHandlerWithID = async ({
data: req.data,
depth: isNumber(depth) ? Number(depth) : undefined,
draft,
overrideLock: Boolean(overrideLock === 'true'),
publishSpecificLocale,
req,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/views/Edit/Default/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export const DefaultEditView: React.FC = () => {
const shouldShowDocumentLockedModal =
documentIsLocked &&
currentEditor &&
currentEditor.id !== user.id &&
currentEditor.id !== user?.id &&
!isReadOnlyForIncomingUser &&
!showTakeOverModal &&
!documentLockStateRef.current?.hasShownLockedModal
Expand Down
24 changes: 20 additions & 4 deletions packages/ui/src/elements/DeleteMany/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type Props = {
}

export const DeleteMany: React.FC<Props> = (props) => {
const { collection: { slug, labels: { plural } } = {} } = props
const { collection: { slug, labels: { plural, singular } } = {} } = props

const { permissions } = useAuth()
const {
Expand Down Expand Up @@ -65,8 +65,22 @@ export const DeleteMany: React.FC<Props> = (props) => {
try {
const json = await res.json()
toggleModal(modalSlug)
if (res.status < 400) {
toast.success(json.message || t('general:deletedSuccessfully'))

const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular

if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:deletedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
}
toggleAll()
router.replace(
stringifyParams({
Expand Down Expand Up @@ -96,11 +110,13 @@ export const DeleteMany: React.FC<Props> = (props) => {
addDefaultError,
api,
getQueryParams,
i18n.language,
i18n,
modalSlug,
plural,
router,
selectAll,
serverURL,
singular,
slug,
stringifyParams,
t,
Expand Down
24 changes: 20 additions & 4 deletions packages/ui/src/elements/PublishMany/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type PublishManyProps = {
export const PublishMany: React.FC<PublishManyProps> = (props) => {
const { clearRouteCache } = useRouteCache()

const { collection: { slug, labels: { plural }, versions } = {} } = props
const { collection: { slug, labels: { plural, singular }, versions } = {} } = props

const {
config: {
Expand Down Expand Up @@ -71,8 +71,22 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
try {
const json = await res.json()
toggleModal(modalSlug)
if (res.status < 400) {
toast.success(t('general:updatedSuccessfully'))

const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular

if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:updatedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
}
router.replace(
stringifyParams({
params: {
Expand All @@ -99,10 +113,12 @@ export const PublishMany: React.FC<PublishManyProps> = (props) => {
addDefaultError,
api,
getQueryParams,
i18n.language,
i18n,
modalSlug,
plural,
selectAll,
serverURL,
singular,
slug,
t,
toggleModal,
Expand Down
24 changes: 20 additions & 4 deletions packages/ui/src/elements/UnpublishMany/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type UnpublishManyProps = {
}

export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
const { collection: { slug, labels: { plural }, versions } = {} } = props
const { collection: { slug, labels: { plural, singular }, versions } = {} } = props

const {
config: {
Expand Down Expand Up @@ -69,8 +69,22 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
try {
const json = await res.json()
toggleModal(modalSlug)
if (res.status < 400) {
toast.success(t('general:updatedSuccessfully'))

const deletedDocs = json?.docs.length || 0
const successLabel = deletedDocs > 1 ? plural : singular

if (res.status < 400 || deletedDocs > 0) {
toast.success(
t('general:updatedCountSuccessfully', {
count: deletedDocs,
label: getTranslation(successLabel, i18n),
}),
)
if (json?.errors.length > 0) {
toast.error(json.message, {
description: json.errors.map((error) => error.message).join('\n'),
})
}
router.replace(
stringifyParams({
params: {
Expand All @@ -96,10 +110,12 @@ export const UnpublishMany: React.FC<UnpublishManyProps> = (props) => {
addDefaultError,
api,
getQueryParams,
i18n.language,
i18n,
modalSlug,
plural,
selectAll,
serverURL,
singular,
slug,
t,
toggleModal,
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/providers/Selection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
)

const getQueryParams = useCallback(
(additionalParams?: Where): string => {
(additionalWhereParams?: Where): string => {
let where: Where
if (selectAll === SelectAllStatus.AllAvailable) {
const params = searchParams?.where as Where
Expand All @@ -126,9 +126,9 @@ export const SelectionProvider: React.FC<Props> = ({ children, docs = [], totalD
},
}
}
if (additionalParams) {
if (additionalWhereParams) {
where = {
and: [{ ...additionalParams }, where],
and: [{ ...additionalWhereParams }, where],
}
}
return qs.stringify(
Expand Down
118 changes: 115 additions & 3 deletions test/locked-documents/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ import type { TypeWithID } from 'payload'

import { expect, test } from '@playwright/test'
import * as path from 'path'
import { mapAsync } from 'payload'
import { wait } from 'payload/shared'
import { fileURLToPath } from 'url'

import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config } from './payload-types.js'

import { ensureCompilationIsDone, initPageConsoleErrorCatch, saveDocAndAssert } from '../helpers.js'
import {
ensureCompilationIsDone,
exactText,
initPageConsoleErrorCatch,
saveDocAndAssert,
} from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { TEST_TIMEOUT } from '../playwright.config.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT } from '../playwright.config.js'
import { postsSlug } from './collections/Posts/index.js'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
Expand Down Expand Up @@ -160,14 +167,115 @@ describe('locked documents', () => {
await expect(page.locator('.table .row-1 .checkbox-input__input')).toBeVisible()
})

test('should only allow bulk delete on unlocked documents', async () => {
test('should only allow bulk delete on unlocked documents on current page', async () => {
await page.goto(postsUrl.list)
await page.locator('input#select-all').check()
await page.locator('.delete-documents__toggle').click()
await expect(page.locator('.delete-documents__content p')).toHaveText(
'You are about to delete 2 Posts',
)
})

test('should only allow bulk delete on unlocked documents on all pages', async () => {
await mapAsync([...Array(9)], async () => {
await createPostDoc({
text: 'Ready for delete',
})
})

await page.reload()

await page.goto(postsUrl.list)
await page.waitForURL(new RegExp(postsUrl.list))

await page.locator('input#select-all').check()
await page.locator('.list-selection .list-selection__button').click()
await page.locator('.delete-documents__toggle').click()
await page.locator('#confirm-delete').click()
await expect(page.locator('.cell-_select')).toHaveCount(1)
})

test('should only allow bulk publish on unlocked documents on all pages', async () => {
await mapAsync([...Array(10)], async () => {
await createPostDoc({
text: 'Ready for delete',
})
})

await page.reload()

await page.goto(postsUrl.list)
await page.waitForURL(new RegExp(postsUrl.list))

await page.locator('input#select-all').check()
await page.locator('.list-selection .list-selection__button').click()
await page.locator('.publish-many__toggle').click()
await page.locator('#confirm-publish').click()

const paginator = page.locator('.paginator')

await paginator.locator('button').nth(1).click()
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
await expect(page.locator('.row-1 .cell-_status')).toContainText('Draft')
})

test('should only allow bulk unpublish on unlocked documents on all pages', async () => {
await page.goto(postsUrl.list)
await page.waitForURL(new RegExp(postsUrl.list))

await page.locator('input#select-all').check()
await page.locator('.list-selection .list-selection__button').click()
await page.locator('.unpublish-many__toggle').click()
await page.locator('#confirm-unpublish').click()
await expect(page.locator('.payload-toast-container .toast-success')).toHaveText(
'Updated 10 Posts successfully.',
)
})

test('should only allow bulk edit on unlocked documents on all pages', async () => {
await page.goto(postsUrl.list)
await page.waitForURL(new RegExp(postsUrl.list))

const bulkText = 'Bulk update title'

await page.locator('input#select-all').check()
await page.locator('.list-selection .list-selection__button').click()
await page.locator('.edit-many__toggle').click()

await page.locator('.field-select .rs__control').click()

const textOption = page.locator('.field-select .rs__option', {
hasText: exactText('Text'),
})

await expect(textOption).toBeVisible()

await textOption.click()

const textInput = page.locator('#field-text')

await expect(textInput).toBeVisible()

await textInput.fill(bulkText)

await page.locator('.form-submit button[type="submit"].edit-many__publish').click()
await expect(page.locator('.payload-toast-container .toast-error')).toContainText(
'Unable to update 1 out of 11 Posts.',
)

await page.locator('.edit-many__header__close').click()

await page.reload()

await expect(page.locator('.row-1 .cell-text')).toContainText(bulkText)
await expect(page.locator('.row-2 .cell-text')).toContainText(bulkText)

const paginator = page.locator('.paginator')

await paginator.locator('button').nth(1).click()
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).toContain('page=2')
await expect(page.locator('.row-1 .cell-text')).toContainText('hello')
})
})

describe('document locking / unlocking - one user', () => {
Expand Down Expand Up @@ -899,3 +1007,7 @@ async function createPageDoc(data: any): Promise<Record<string, unknown> & TypeW
data,
}) as unknown as Promise<Record<string, unknown> & TypeWithID>
}

async function deleteAllPosts() {
await payload.delete({ collection: postsSlug, where: { id: { exists: true } } })
}