Added support for creating new tags when bulk editing posts

refs https://github.com/TryGhost/Team/issues/2922
This commit is contained in:
Simon Backx 2023-04-13 17:05:42 +02:00
parent 76fae2a724
commit 788aa34c8b
3 changed files with 80 additions and 14 deletions

View File

@ -105,10 +105,49 @@ export default class PostsContextMenu extends Component {
*addTagToPostsTask(tags) { *addTagToPostsTask(tags) {
const updatedModels = this.selectionList.availableModels; const updatedModels = this.selectionList.availableModels;
yield this.performBulkEdit('addTag', {tags: tags.map(tag => tag.id)}); yield this.performBulkEdit('addTag', {
tags: tags.map((t) => {
return {
id: t.id,
name: t.name,
slug: t.slug
};
})
});
this.notifications.showNotification(this.#getToastMessage('tagsAdded'), {type: 'success'}); this.notifications.showNotification(this.#getToastMessage('tagsAdded'), {type: 'success'});
const serializedTags = tags.toArray().map((t) => {
return {
...t.serialize({includeId: true}),
type: 'tag'
};
});
// Destroy unsaved new tags (otherwise we could select them again)
this.store.peekAll('tag').forEach((tag) => {
if (tag.isNew) {
tag.destroyRecord();
}
});
// For new tags, attach the id to it, so we can link the new tag to the post
let allTags = null;
for (const tag of serializedTags) {
if (!tag.id) {
if (!allTags) {
// Update tags on the client side (we could have created new tags)
yield this.store.query('tag', {limit: 'all'});
allTags = this.store.peekAll('tag').toArray();
}
const createdTag = allTags.find(t => t.name === tag.name && t.id);
if (createdTag) {
tag.id = createdTag.id;
tag.slug = createdTag.slug;
}
}
}
// Update the models on the client side // Update the models on the client side
for (const post of updatedModels) { for (const post of updatedModels) {
const newTags = post.tags.toArray().map((t) => { const newTags = post.tags.toArray().map((t) => {
@ -117,12 +156,9 @@ export default class PostsContextMenu extends Component {
type: 'tag' type: 'tag'
}; };
}); });
for (const tag of tags) { for (const tag of serializedTags) {
if (!newTags.find(t => t.id === tag.id)) { if (!newTags.find(t => t.id === tag.id)) {
newTags.push({ newTags.push(tag);
...tag.serialize({includeId: true}),
type: 'tag'
});
} }
} }

View File

@ -22,6 +22,13 @@ export default class AddTag extends Component {
// store and be updated when the above query returns // store and be updated when the above query returns
this.store.query('tag', {limit: 'all'}); this.store.query('tag', {limit: 'all'});
this.#availableTags = this.store.peekAll('tag'); this.#availableTags = this.store.peekAll('tag');
// Destroy unsaved new tags (otherwise we could select them again -> create them again)
this.#availableTags.forEach((tag) => {
if (tag.isNew) {
tag.destroyRecord();
}
});
} }
@action @action
@ -58,11 +65,8 @@ export default class AddTag extends Component {
} }
@action @action
shouldAllowCreate() { shouldAllowCreate(nameInput) {
return false; return !this.#findTagByName(nameInput.trim(), this.#availableTags);
// This is not supported by the backend yet
// return !this.#findTagByName(nameInput.trim(), this.#availableTags);
} }
#findTagByName(name, tags) { #findTagByName(name, tags) {

View File

@ -8,6 +8,7 @@ const messages = {
invalidVisibilityFilter: 'Invalid visibility filter.', invalidVisibilityFilter: 'Invalid visibility filter.',
invalidVisibility: 'Invalid visibility value.', invalidVisibility: 'Invalid visibility value.',
invalidTiers: 'Invalid tiers value.', invalidTiers: 'Invalid tiers value.',
invalidTags: 'Invalid tags value.',
invalidEmailSegment: 'The email segment parameter doesn\'t contain a valid filter', invalidEmailSegment: 'The email segment parameter doesn\'t contain a valid filter',
unsupportedBulkAction: 'Unsupported bulk action' unsupportedBulkAction: 'Unsupported bulk action'
}; };
@ -95,6 +96,23 @@ class PostsService {
return await this.#updatePosts({visibility: data.meta.visibility, tiers}, {filter: options.filter}); return await this.#updatePosts({visibility: data.meta.visibility, tiers}, {filter: options.filter});
} }
if (data.action === 'addTag') { if (data.action === 'addTag') {
if (!Array.isArray(data.meta.tags)) {
throw new errors.IncorrectUsageError({
message: tpl(messages.invalidTags)
});
}
for (const tag of data.meta.tags) {
if (typeof tag !== 'object') {
throw new errors.IncorrectUsageError({
message: tpl(messages.invalidTags)
});
}
if (!tag.id && !tag.name) {
throw new errors.IncorrectUsageError({
message: tpl(messages.invalidTags)
});
}
}
return await this.#bulkAddTags({tags: data.meta.tags}, {filter: options.filter}); return await this.#bulkAddTags({tags: data.meta.tags}, {filter: options.filter});
} }
throw new errors.IncorrectUsageError({ throw new errors.IncorrectUsageError({
@ -118,17 +136,25 @@ class PostsService {
}); });
} }
// Create tags that don't exist
for (const tag of data.tags) {
if (!tag.id) {
const createdTag = await this.models.Tag.add(tag, {transacting: options.transacting});
tag.id = createdTag.id;
}
}
const postRows = await this.models.Post.getFilteredCollectionQuery({ const postRows = await this.models.Post.getFilteredCollectionQuery({
filter: options.filter, filter: options.filter,
status: 'all' status: 'all'
}).select('posts.id'); }).select('posts.id');
const postTags = data.tags.reduce((pt, tagId) => { const postTags = data.tags.reduce((pt, tag) => {
return pt.concat(postRows.map((post) => { return pt.concat(postRows.map((post) => {
return { return {
id: (new ObjectId()).toHexString(), id: (new ObjectId()).toHexString(),
post_id: post.id, post_id: post.id,
tag_id: tagId, tag_id: tag.id,
sort_order: 0 sort_order: 0
}; };
})); }));