mirror of
https://github.com/strapi/strapi.git
synced 2025-09-21 14:31:16 +00:00
Add bulk publish and unpublish
This commit is contained in:
parent
1347bb4f26
commit
119b88a1b1
@ -240,10 +240,10 @@ const BulkActionsBar = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmUnpublishAll = async () => {
|
||||
const handleConfirmUnpublishAll = () => {
|
||||
try {
|
||||
setIsConfirmButtonLoading(true);
|
||||
await onConfirmUnpublishAll(selectedEntries);
|
||||
onConfirmUnpublishAll(selectedEntries);
|
||||
clearSelectedEntries();
|
||||
setIsConfirmButtonLoading(false);
|
||||
} catch (err) {
|
||||
|
@ -3,7 +3,7 @@ import { act, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ThemeProvider, lightTheme } from '@strapi/design-system';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import BulkActionsBar from '../index';
|
||||
import BulkActionsBar from '../index'
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
|
||||
import { ArrowLeft, Plus, Cog } from '@strapi/icons';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import DynamicTable from '../../components/DynamicTable';
|
||||
import AttributeFilter from '../../components/AttributeFilter';
|
||||
@ -102,6 +103,14 @@ function ListView({
|
||||
const fetchClient = useFetchClient();
|
||||
const { post, del } = fetchClient;
|
||||
|
||||
const bulkAction = async ({ query, input }) => {
|
||||
const { data } = await post(query, { ...input });
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const bulkPublishMutation = useMutation(bulkAction);
|
||||
|
||||
// FIXME
|
||||
// Using a ref to avoid requests being fired multiple times on slug on change
|
||||
// We need it because the hook as mulitple dependencies so it may run before the permissions have checked
|
||||
@ -180,21 +189,6 @@ function ListView({
|
||||
[fetchData, params, slug, toggleNotification, formatAPIError, post]
|
||||
);
|
||||
|
||||
const handleConfirmPublishAllData = async (selectedEntries) => {
|
||||
const validations = await validateEntriesToPublish(selectedEntries);
|
||||
console.log('Validations', validations);
|
||||
|
||||
if (validations.errors.length > 0) {
|
||||
// TODO make a request to the API and refetch the data
|
||||
console.info('Publishing all data', selectedEntries);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmUnpublishAllData = (ids) => {
|
||||
// TODO make a request to the API and refetch the data
|
||||
console.info('Unpublishing all data', ids);
|
||||
};
|
||||
|
||||
const handleConfirmDeleteData = useCallback(
|
||||
async (idToDelete) => {
|
||||
try {
|
||||
@ -254,6 +248,76 @@ function ListView({
|
||||
return validations;
|
||||
};
|
||||
|
||||
const handleConfirmPublishAllData = async (selectedEntries) => {
|
||||
const validations = await validateEntriesToPublish(selectedEntries);
|
||||
|
||||
if (Object.values(validations.errors).length) {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
title: {
|
||||
id: 'content-manager.listView.validation.errors.title',
|
||||
defaultMessage: 'Action required',
|
||||
},
|
||||
message: {
|
||||
id: 'content-manager.listView.validation.errors.message',
|
||||
defaultMessage:
|
||||
'Please make sure all fields are valid before publishing (required field, min/max character limit, etc.)',
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bulkPublishMutation.mutate(
|
||||
{
|
||||
query: `/content-manager/collection-types/${contentType.uid}/actions/bulkPublish`,
|
||||
input: { ids: selectedEntries },
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
fetchData(`/content-manager/collection-types/${slug}${params}`);
|
||||
toggleNotification({
|
||||
type: 'success',
|
||||
message: { id: 'content-manager.success.record.publish', defaultMessage: 'Published' },
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatAPIError(error),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleConfirmUnpublishAllData = (selectedEntries) => {
|
||||
bulkPublishMutation.mutate(
|
||||
{
|
||||
query: `/content-manager/collection-types/${contentType.uid}/actions/bulkUnpublish`,
|
||||
input: { ids: selectedEntries },
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
fetchData(`/content-manager/collection-types/${slug}${params}`);
|
||||
toggleNotification({
|
||||
type: 'success',
|
||||
message: {
|
||||
id: 'content-manager.success.record.unpublish',
|
||||
defaultMessage: 'Unpublished',
|
||||
},
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatAPIError(error),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const CancelToken = axios.CancelToken;
|
||||
const source = CancelToken.source();
|
||||
@ -417,6 +481,7 @@ ListView.propTypes = {
|
||||
layout: PropTypes.exact({
|
||||
components: PropTypes.object.isRequired,
|
||||
contentType: PropTypes.shape({
|
||||
uid: PropTypes.string.isRequired,
|
||||
attributes: PropTypes.object.isRequired,
|
||||
metadatas: PropTypes.object.isRequired,
|
||||
info: PropTypes.shape({ displayName: PropTypes.string.isRequired }).isRequired,
|
||||
|
@ -817,6 +817,8 @@
|
||||
"content-manager.success.record.save": "Saved",
|
||||
"content-manager.success.record.unpublish": "Unpublished",
|
||||
"content-manager.utils.data-loaded": "The {number, plural, =1 {entry has} other {entries have}} successfully been loaded",
|
||||
"content-manager.listView.validation.errors.title": "Action required",
|
||||
"content-manager.listView.validation.errors.message": "Please make sure all fields are valid before publishing (required field, min/max character limit, etc.)",
|
||||
"dark": "Dark",
|
||||
"form.button.continue": "Continue",
|
||||
"form.button.done": "Done",
|
||||
|
@ -3,7 +3,7 @@
|
||||
const { setCreatorFields, pipeAsync } = require('@strapi/utils');
|
||||
|
||||
const { getService, pickWritableAttributes } = require('../utils');
|
||||
const { validateBulkDeleteInput } = require('./validation');
|
||||
const { validateBulkActionInput } = require('./validation');
|
||||
|
||||
module.exports = {
|
||||
async find(ctx) {
|
||||
@ -181,6 +181,64 @@ module.exports = {
|
||||
ctx.body = await permissionChecker.sanitizeOutput(result);
|
||||
},
|
||||
|
||||
async bulkPublish(ctx) {
|
||||
const { userAbility } = ctx.state;
|
||||
const { model } = ctx.params;
|
||||
const { query, body } = ctx.request;
|
||||
const { ids } = body;
|
||||
|
||||
await validateBulkActionInput(body);
|
||||
|
||||
const entityManager = getService('entity-manager');
|
||||
const permissionChecker = getService('permission-checker').create({ userAbility, model });
|
||||
|
||||
if (permissionChecker.cannot.publish()) {
|
||||
return ctx.forbidden();
|
||||
}
|
||||
|
||||
const permissionQuery = await permissionChecker.sanitizedQuery.publish(query);
|
||||
|
||||
const idsWhereClause = { id: { $in: ids } };
|
||||
const params = {
|
||||
...permissionQuery,
|
||||
filters: {
|
||||
$and: [idsWhereClause].concat(permissionQuery.filters || []),
|
||||
},
|
||||
};
|
||||
|
||||
const { count } = await entityManager.publishMany(params, model);
|
||||
ctx.body = { count };
|
||||
},
|
||||
|
||||
async bulkUnpublish(ctx) {
|
||||
const { userAbility } = ctx.state;
|
||||
const { model } = ctx.params;
|
||||
const { query, body } = ctx.request;
|
||||
const { ids } = body;
|
||||
|
||||
await validateBulkActionInput(body);
|
||||
|
||||
const entityManager = getService('entity-manager');
|
||||
const permissionChecker = getService('permission-checker').create({ userAbility, model });
|
||||
|
||||
if (permissionChecker.cannot.unpublish()) {
|
||||
return ctx.forbidden();
|
||||
}
|
||||
|
||||
const permissionQuery = await permissionChecker.sanitizedQuery.unpublish(query);
|
||||
|
||||
const idsWhereClause = { id: { $in: ids } };
|
||||
const params = {
|
||||
...permissionQuery,
|
||||
filters: {
|
||||
$and: [idsWhereClause].concat(permissionQuery.filters || []),
|
||||
},
|
||||
};
|
||||
|
||||
const { count } = await entityManager.unpublishMany(params, model);
|
||||
ctx.body = { count };
|
||||
},
|
||||
|
||||
async unpublish(ctx) {
|
||||
const { userAbility, user } = ctx.state;
|
||||
const { id, model } = ctx.params;
|
||||
@ -217,7 +275,7 @@ module.exports = {
|
||||
const { query, body } = ctx.request;
|
||||
const { ids } = body;
|
||||
|
||||
await validateBulkDeleteInput(body);
|
||||
await validateBulkActionInput(body);
|
||||
|
||||
const entityManager = getService('entity-manager');
|
||||
const permissionChecker = getService('permission-checker').create({ userAbility, model });
|
||||
|
@ -13,7 +13,7 @@ const TYPES = ['singleType', 'collectionType'];
|
||||
*/
|
||||
const kindSchema = yup.string().oneOf(TYPES).nullable();
|
||||
|
||||
const bulkDeleteInputSchema = yup
|
||||
const bulkActionInputSchema = yup
|
||||
.object({
|
||||
ids: yup.array().of(yup.strapiID()).min(1).required(),
|
||||
})
|
||||
@ -64,7 +64,7 @@ const validatePagination = ({ page, pageSize }) => {
|
||||
module.exports = {
|
||||
createModelConfigurationSchema,
|
||||
validateKind: validateYupSchema(kindSchema),
|
||||
validateBulkDeleteInput: validateYupSchema(bulkDeleteInputSchema),
|
||||
validateBulkActionInput: validateYupSchema(bulkActionInputSchema),
|
||||
validateGenerateUIDInput: validateYupSchema(generateUIDInputSchema),
|
||||
validateCheckUIDAvailabilityInput: validateYupSchema(checkUIDAvailabilityInputSchema),
|
||||
validateUIDField,
|
||||
|
@ -323,6 +323,38 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/collection-types/:model/actions/bulkPublish',
|
||||
handler: 'collection-types.bulkPublish',
|
||||
config: {
|
||||
middlewares: [routing],
|
||||
policies: [
|
||||
'plugin::content-manager.has-draft-and-publish',
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'plugin::content-manager.hasPermissions',
|
||||
config: { actions: ['plugin::content-manager.explorer.delete'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/collection-types/:model/actions/bulkUnpublish',
|
||||
handler: 'collection-types.bulkUnpublish',
|
||||
config: {
|
||||
middlewares: [routing],
|
||||
policies: [
|
||||
'plugin::content-manager.has-draft-and-publish',
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'plugin::content-manager.hasPermissions',
|
||||
config: { actions: ['plugin::content-manager.explorer.delete'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/collection-types/:model/:id/actions/numberOfDraftRelations',
|
||||
|
@ -5,6 +5,7 @@ const entityManagerLoader = require('../entity-manager');
|
||||
|
||||
let entityManager;
|
||||
|
||||
const queryUpdateMock = jest.fn(() => Promise.resolve());
|
||||
describe('Content-Manager', () => {
|
||||
const fakeModel = {
|
||||
modelName: 'fake model',
|
||||
@ -18,8 +19,15 @@ describe('Content-Manager', () => {
|
||||
entityService: {
|
||||
update: jest.fn().mockReturnValue({ id: 1, publishedAt: new Date() }),
|
||||
},
|
||||
db: {
|
||||
query: jest.fn(() => ({
|
||||
findMany: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
|
||||
updateMany: queryUpdateMock,
|
||||
})),
|
||||
},
|
||||
entityValidator: {
|
||||
validateEntityCreation() {},
|
||||
validateEntityUpdate: jest.fn().mockReturnValue([{ id: 1 }, { id: 2 }]),
|
||||
},
|
||||
eventHub: { emit: jest.fn(), sanitizeEntity: (entity) => entity },
|
||||
getModel: jest.fn(() => fakeModel),
|
||||
@ -44,12 +52,32 @@ describe('Content-Manager', () => {
|
||||
populate: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('Publish many content-types', async () => {
|
||||
const uid = 'api::test.test';
|
||||
const params = { filters: { $and: [1, 2] } };
|
||||
|
||||
await entityManager.publishMany(params, uid);
|
||||
|
||||
expect(strapi.db.query().updateMany).toBeCalledWith({
|
||||
where: {
|
||||
$and: [1, 2],
|
||||
},
|
||||
data: { publishedAt: expect.any(Date) },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unpublish', () => {
|
||||
const defaultConfig = {};
|
||||
beforeEach(() => {
|
||||
global.strapi = {
|
||||
db: {
|
||||
query: jest.fn(() => ({
|
||||
findMany: jest.fn().mockResolvedValue([{ id: 1 }, { id: 2 }]),
|
||||
updateMany: queryUpdateMock,
|
||||
})),
|
||||
},
|
||||
entityService: {
|
||||
update: jest.fn().mockReturnValue({ id: 1, publishedAt: null }),
|
||||
},
|
||||
@ -76,5 +104,19 @@ describe('Content-Manager', () => {
|
||||
populate: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('Unpublish many content-types', async () => {
|
||||
const uid = 'api::test.test';
|
||||
const params = { filters: { $and: [1, 2] } };
|
||||
|
||||
await entityManager.unpublishMany(params, uid);
|
||||
|
||||
expect(strapi.db.query().updateMany).toBeCalledWith({
|
||||
where: {
|
||||
$and: [1, 2],
|
||||
},
|
||||
data: { publishedAt: null },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,6 +3,7 @@
|
||||
const { assoc, has, prop, omit } = require('lodash/fp');
|
||||
const strapiUtils = require('@strapi/utils');
|
||||
const { mapAsync } = require('@strapi/utils');
|
||||
const { transformParamsToQuery } = require('@strapi/utils').convertQueryParams;
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
const { getDeepPopulate, getDeepPopulateDraftCount } = require('./utils/populate');
|
||||
const { getDeepRelationsCount } = require('./utils/count');
|
||||
@ -266,6 +267,77 @@ module.exports = ({ strapi }) => ({
|
||||
return mappedEntity;
|
||||
},
|
||||
|
||||
async publishMany(opts, uid) {
|
||||
const params = {
|
||||
...opts,
|
||||
data: {
|
||||
[PUBLISHED_AT_ATTRIBUTE]: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
const query = transformParamsToQuery(uid, params);
|
||||
const entitiesToUpdate = await strapi.db.query(uid).findMany(query);
|
||||
// No entities to update, return early
|
||||
if (!entitiesToUpdate.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate entities before publishing, throw if invalid
|
||||
await Promise.all(
|
||||
entitiesToUpdate.map((entityToUpdate) =>
|
||||
strapi.entityValidator.validateEntityCreation(
|
||||
strapi.getModel(uid),
|
||||
entityToUpdate,
|
||||
{
|
||||
isDraft: true,
|
||||
},
|
||||
entityToUpdate
|
||||
)
|
||||
)
|
||||
);
|
||||
// Everything is valid, publish
|
||||
const publishedEntitiesCount = await strapi.db
|
||||
.query(uid)
|
||||
.updateMany({ ...query, data: params.data });
|
||||
|
||||
// Get the updated entities since updateMany only returns the count
|
||||
const publishedEntities = await strapi.db.query(uid).findMany(query);
|
||||
// Emit the publish event for all updated entities
|
||||
await Promise.all(publishedEntities.map((entity) => emitEvent(ENTRY_PUBLISH, entity, uid)));
|
||||
|
||||
// Return the number of published entities
|
||||
return publishedEntitiesCount;
|
||||
},
|
||||
|
||||
async unpublishMany(opts, uid) {
|
||||
const params = {
|
||||
...opts,
|
||||
data: {
|
||||
[PUBLISHED_AT_ATTRIBUTE]: null,
|
||||
},
|
||||
};
|
||||
|
||||
const query = transformParamsToQuery(uid, params);
|
||||
const entitiesToUpdate = await strapi.db.query(uid).findMany(query);
|
||||
// No entities to update, return early
|
||||
if (!entitiesToUpdate.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// No need to validate, unpublish
|
||||
const unpublishedEntitiesCount = await strapi.db
|
||||
.query(uid)
|
||||
.updateMany({ ...query, data: params.data });
|
||||
|
||||
// Get the updated entities since updateMany only returns the count
|
||||
const unpublishedEntities = await strapi.db.query(uid).findMany(query);
|
||||
// Emit the unpublish event for all updated entities
|
||||
await Promise.all(unpublishedEntities.map((entity) => emitEvent(ENTRY_UNPUBLISH, entity, uid)));
|
||||
|
||||
// Return the number of unpublished entities
|
||||
return unpublishedEntitiesCount;
|
||||
},
|
||||
|
||||
async unpublish(entity, body = {}, uid) {
|
||||
if (!entity[PUBLISHED_AT_ATTRIBUTE]) {
|
||||
throw new ApplicationError('already.draft');
|
||||
|
Loading…
x
Reference in New Issue
Block a user