mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
feat: Store comments total per card in database
This commit is contained in:
parent
aea482ba03
commit
d1707ab1ae
7 changed files with 159 additions and 39 deletions
|
@ -49,11 +49,6 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
[],
|
||||
);
|
||||
|
||||
const selectCommentsTotalByCardId = useMemo(
|
||||
() => selectors.makeSelectCommentsTotalByCardId(),
|
||||
[],
|
||||
);
|
||||
|
||||
const selectAttachmentById = useMemo(() => selectors.makeSelectAttachmentById(), []);
|
||||
|
||||
const card = useSelector((state) => selectCardById(state, cardId));
|
||||
|
@ -75,8 +70,6 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
selectNotificationsTotalByCardId(state, cardId),
|
||||
);
|
||||
|
||||
const commentsTotal = useSelector((state) => selectCommentsTotalByCardId(state, cardId));
|
||||
|
||||
const coverUrl = useSelector((state) => {
|
||||
const attachment = selectAttachmentById(state, card.coverAttachmentId);
|
||||
return attachment && attachment.data.thumbnailUrls.outside360;
|
||||
|
@ -121,9 +114,9 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
card.description ||
|
||||
card.dueDate ||
|
||||
card.stopwatch ||
|
||||
card.commentsTotal > 0 ||
|
||||
attachmentsTotal > 0 ||
|
||||
notificationsTotal > 0 ||
|
||||
commentsTotal > 0 ||
|
||||
listName;
|
||||
|
||||
const isCompact =
|
||||
|
@ -234,11 +227,11 @@ const ProjectContent = React.memo(({ cardId }) => {
|
|||
</span>
|
||||
</span>
|
||||
)}
|
||||
{commentsTotal > 0 && (
|
||||
{card.commentsTotal > 0 && (
|
||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||
<span className={styles.attachmentContent}>
|
||||
<Icon name="comment outline" />
|
||||
{commentsTotal}
|
||||
{card.commentsTotal}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
|
|
@ -20,6 +20,9 @@ export default class extends BaseModel {
|
|||
description: attr(),
|
||||
dueDate: attr(),
|
||||
stopwatch: attr(),
|
||||
commentsTotal: attr({
|
||||
getDefault: () => 0,
|
||||
}),
|
||||
createdAt: attr({
|
||||
getDefault: () => new Date(),
|
||||
}),
|
||||
|
|
|
@ -42,31 +42,47 @@ export default class extends BaseModel {
|
|||
|
||||
break;
|
||||
case ActionTypes.COMMENT_CREATE:
|
||||
case ActionTypes.COMMENT_CREATE_HANDLE:
|
||||
case ActionTypes.COMMENT_UPDATE__SUCCESS:
|
||||
case ActionTypes.COMMENT_UPDATE_HANDLE:
|
||||
Comment.upsert(payload.comment);
|
||||
case ActionTypes.COMMENT_CREATE_HANDLE: {
|
||||
const commentModel = Comment.upsert(payload.comment);
|
||||
|
||||
if (commentModel.card) {
|
||||
commentModel.card.commentsTotal += 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ActionTypes.COMMENT_CREATE__SUCCESS:
|
||||
Comment.withId(payload.localId).delete();
|
||||
Comment.upsert(payload.comment);
|
||||
|
||||
break;
|
||||
case ActionTypes.COMMENT_CREATE__FAILURE:
|
||||
Comment.withId(payload.localId).delete();
|
||||
case ActionTypes.COMMENT_CREATE__FAILURE: {
|
||||
const commentModel = Comment.withId(payload.localId);
|
||||
commentModel.delete();
|
||||
|
||||
if (commentModel.card) {
|
||||
commentModel.card.commentsTotal -= 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ActionTypes.COMMENT_UPDATE:
|
||||
Comment.withId(payload.id).update(payload.data);
|
||||
|
||||
break;
|
||||
case ActionTypes.COMMENT_DELETE:
|
||||
Comment.withId(payload.id).delete();
|
||||
case ActionTypes.COMMENT_UPDATE__SUCCESS:
|
||||
case ActionTypes.COMMENT_UPDATE_HANDLE:
|
||||
Comment.upsert(payload.comment);
|
||||
|
||||
break;
|
||||
case ActionTypes.COMMENT_DELETE__SUCCESS:
|
||||
case ActionTypes.COMMENT_DELETE_HANDLE: {
|
||||
case ActionTypes.COMMENT_DELETE: {
|
||||
const commentModel = Comment.withId(payload.id);
|
||||
commentModel.delete();
|
||||
commentModel.card.commentsTotal -= 1;
|
||||
|
||||
break;
|
||||
}
|
||||
case ActionTypes.COMMENT_DELETE__SUCCESS: {
|
||||
const commentModel = Comment.withId(payload.comment.id);
|
||||
|
||||
if (commentModel) {
|
||||
|
@ -75,6 +91,19 @@ export default class extends BaseModel {
|
|||
|
||||
break;
|
||||
}
|
||||
case ActionTypes.COMMENT_DELETE_HANDLE: {
|
||||
const commentModel = Comment.withId(payload.comment.id);
|
||||
|
||||
if (commentModel) {
|
||||
commentModel.delete();
|
||||
|
||||
if (commentModel.card) {
|
||||
commentModel.card.commentsTotal -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,23 +28,7 @@ export const makeSelectCommentById = () =>
|
|||
|
||||
export const selectCommentById = makeSelectCommentById();
|
||||
|
||||
export const makeSelectCommentsTotalByCardId = () =>
|
||||
createSelector(
|
||||
orm,
|
||||
(_, cardId) => cardId,
|
||||
({ Card }, cardId) => {
|
||||
const cardModel = Card.withId(cardId);
|
||||
|
||||
if (!cardModel) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return cardModel.comments.count();
|
||||
},
|
||||
);
|
||||
|
||||
export default {
|
||||
makeSelectCommentById,
|
||||
selectCommentById,
|
||||
makeSelectCommentsTotalByCardId,
|
||||
};
|
||||
|
|
|
@ -10,7 +10,25 @@ const defaultFind = (criteria, { limit } = {}) =>
|
|||
|
||||
/* Query methods */
|
||||
|
||||
const createOne = (values) => Comment.create({ ...values }).fetch();
|
||||
const createOne = (values) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const comment = await Comment.create({ ...values })
|
||||
.fetch()
|
||||
.usingConnection(db);
|
||||
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE card SET comments_total = comments_total + 1, updated_at = $1 WHERE id = $2',
|
||||
[new Date().toISOString(), comment.cardId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount === 0) {
|
||||
throw 'cardNotFound';
|
||||
}
|
||||
|
||||
return comment;
|
||||
});
|
||||
|
||||
const getByIds = (ids) => defaultFind(ids);
|
||||
|
||||
|
@ -35,9 +53,65 @@ const update = (criteria, values) => Comment.update(criteria).set(values).fetch(
|
|||
const updateOne = (criteria, values) => Comment.updateOne(criteria).set({ ...values });
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const delete_ = (criteria) => Comment.destroy(criteria).fetch();
|
||||
const delete_ = (criteria) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const comments = await Comment.destroy(criteria).fetch().usingConnection(db);
|
||||
|
||||
const deleteOne = (criteria) => Comment.destroyOne(criteria);
|
||||
if (comments.length > 0) {
|
||||
const commentsByCardId = _.groupBy(comments, 'cardId');
|
||||
|
||||
const cardIdsByTotal = Object.entries(commentsByCardId).reduce(
|
||||
(result, [cardId, commentsItem]) => ({
|
||||
...result,
|
||||
[commentsItem.length]: [...(result[commentsItem.length] || []), cardId],
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const queryValues = [];
|
||||
let query = 'UPDATE card SET comments_total = comments_total - CASE ';
|
||||
|
||||
Object.entries(cardIdsByTotal).forEach(([total, cardIds]) => {
|
||||
const inValues = cardIds.map((cardId) => {
|
||||
queryValues.push(cardId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(total);
|
||||
query += `WHEN id IN (${inValues.join(', ')}) THEN $${queryValues.length}::int `;
|
||||
});
|
||||
|
||||
const inValues = Object.keys(commentsByCardId).map((cardId) => {
|
||||
queryValues.push(cardId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(new Date().toISOString());
|
||||
query += `END, updated_at = $${queryValues.length} WHERE id IN (${inValues.join(', ')})`;
|
||||
|
||||
await sails.sendNativeQuery(query, queryValues).usingConnection(db);
|
||||
}
|
||||
|
||||
return comments;
|
||||
});
|
||||
|
||||
const deleteOne = (criteria) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const comment = await Comment.destroyOne(criteria).usingConnection(db);
|
||||
|
||||
const queryResult = await sails
|
||||
.sendNativeQuery(
|
||||
'UPDATE card SET comments_total = comments_total - 1, updated_at = $1 WHERE id = $2',
|
||||
[new Date().toISOString(), comment.cardId],
|
||||
)
|
||||
.usingConnection(db);
|
||||
|
||||
if (queryResult.rowCount === 0) {
|
||||
throw 'cardNotFound';
|
||||
}
|
||||
|
||||
return comment;
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createOne,
|
||||
|
|
|
@ -48,6 +48,11 @@ module.exports = {
|
|||
stopwatch: {
|
||||
type: 'json',
|
||||
},
|
||||
commentsTotal: {
|
||||
type: 'number',
|
||||
defaultsTo: 0,
|
||||
columnName: 'comments_total',
|
||||
},
|
||||
listChangedAt: {
|
||||
type: 'ref',
|
||||
columnName: 'list_changed_at',
|
||||
|
|
32
server/db/migrations/20250523131647_add_comments_counter.js
Normal file
32
server/db/migrations/20250523131647_add_comments_counter.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
exports.up = async (knex) => {
|
||||
await knex.schema.alterTable('card', (table) => {
|
||||
/* Columns */
|
||||
|
||||
table.integer('comments_total').notNullable().defaultTo(0);
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
UPDATE card
|
||||
SET comments_total = comments_total_by_card_id.comments_total
|
||||
FROM (
|
||||
SELECT card_id, COUNT(*) as comments_total
|
||||
FROM comment
|
||||
GROUP BY card_id
|
||||
) AS comments_total_by_card_id
|
||||
WHERE card.id = comments_total_by_card_id.card_id
|
||||
`);
|
||||
|
||||
return knex.schema.alterTable('card', (table) => {
|
||||
table.integer('comments_total').notNullable().alter();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) =>
|
||||
knex.schema.table('card', (table) => {
|
||||
table.dropColumn('comments_total');
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue