1
0
Fork 0
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:
Maksim Eltyshev 2025-05-23 17:27:21 +02:00
parent aea482ba03
commit d1707ab1ae
7 changed files with 159 additions and 39 deletions

View file

@ -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>
)}

View file

@ -20,6 +20,9 @@ export default class extends BaseModel {
description: attr(),
dueDate: attr(),
stopwatch: attr(),
commentsTotal: attr({
getDefault: () => 0,
}),
createdAt: attr({
getDefault: () => new Date(),
}),

View file

@ -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:
}
}

View file

@ -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,
};

View file

@ -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,

View file

@ -48,6 +48,11 @@ module.exports = {
stopwatch: {
type: 'json',
},
commentsTotal: {
type: 'number',
defaultsTo: 0,
columnName: 'comments_total',
},
listChangedAt: {
type: 'ref',
columnName: 'list_changed_at',

View 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');
});