mirror of
https://github.com/plankanban/planka.git
synced 2025-07-19 13:19: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 selectAttachmentById = useMemo(() => selectors.makeSelectAttachmentById(), []);
|
||||||
|
|
||||||
const card = useSelector((state) => selectCardById(state, cardId));
|
const card = useSelector((state) => selectCardById(state, cardId));
|
||||||
|
@ -75,8 +70,6 @@ const ProjectContent = React.memo(({ cardId }) => {
|
||||||
selectNotificationsTotalByCardId(state, cardId),
|
selectNotificationsTotalByCardId(state, cardId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const commentsTotal = useSelector((state) => selectCommentsTotalByCardId(state, cardId));
|
|
||||||
|
|
||||||
const coverUrl = useSelector((state) => {
|
const coverUrl = useSelector((state) => {
|
||||||
const attachment = selectAttachmentById(state, card.coverAttachmentId);
|
const attachment = selectAttachmentById(state, card.coverAttachmentId);
|
||||||
return attachment && attachment.data.thumbnailUrls.outside360;
|
return attachment && attachment.data.thumbnailUrls.outside360;
|
||||||
|
@ -121,9 +114,9 @@ const ProjectContent = React.memo(({ cardId }) => {
|
||||||
card.description ||
|
card.description ||
|
||||||
card.dueDate ||
|
card.dueDate ||
|
||||||
card.stopwatch ||
|
card.stopwatch ||
|
||||||
|
card.commentsTotal > 0 ||
|
||||||
attachmentsTotal > 0 ||
|
attachmentsTotal > 0 ||
|
||||||
notificationsTotal > 0 ||
|
notificationsTotal > 0 ||
|
||||||
commentsTotal > 0 ||
|
|
||||||
listName;
|
listName;
|
||||||
|
|
||||||
const isCompact =
|
const isCompact =
|
||||||
|
@ -234,11 +227,11 @@ const ProjectContent = React.memo(({ cardId }) => {
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{commentsTotal > 0 && (
|
{card.commentsTotal > 0 && (
|
||||||
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
<span className={classNames(styles.attachment, styles.attachmentLeft)}>
|
||||||
<span className={styles.attachmentContent}>
|
<span className={styles.attachmentContent}>
|
||||||
<Icon name="comment outline" />
|
<Icon name="comment outline" />
|
||||||
{commentsTotal}
|
{card.commentsTotal}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -20,6 +20,9 @@ export default class extends BaseModel {
|
||||||
description: attr(),
|
description: attr(),
|
||||||
dueDate: attr(),
|
dueDate: attr(),
|
||||||
stopwatch: attr(),
|
stopwatch: attr(),
|
||||||
|
commentsTotal: attr({
|
||||||
|
getDefault: () => 0,
|
||||||
|
}),
|
||||||
createdAt: attr({
|
createdAt: attr({
|
||||||
getDefault: () => new Date(),
|
getDefault: () => new Date(),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -42,31 +42,47 @@ export default class extends BaseModel {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.COMMENT_CREATE:
|
case ActionTypes.COMMENT_CREATE:
|
||||||
case ActionTypes.COMMENT_CREATE_HANDLE:
|
case ActionTypes.COMMENT_CREATE_HANDLE: {
|
||||||
case ActionTypes.COMMENT_UPDATE__SUCCESS:
|
const commentModel = Comment.upsert(payload.comment);
|
||||||
case ActionTypes.COMMENT_UPDATE_HANDLE:
|
|
||||||
Comment.upsert(payload.comment);
|
if (commentModel.card) {
|
||||||
|
commentModel.card.commentsTotal += 1;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case ActionTypes.COMMENT_CREATE__SUCCESS:
|
case ActionTypes.COMMENT_CREATE__SUCCESS:
|
||||||
Comment.withId(payload.localId).delete();
|
Comment.withId(payload.localId).delete();
|
||||||
Comment.upsert(payload.comment);
|
Comment.upsert(payload.comment);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.COMMENT_CREATE__FAILURE:
|
case ActionTypes.COMMENT_CREATE__FAILURE: {
|
||||||
Comment.withId(payload.localId).delete();
|
const commentModel = Comment.withId(payload.localId);
|
||||||
|
commentModel.delete();
|
||||||
|
|
||||||
|
if (commentModel.card) {
|
||||||
|
commentModel.card.commentsTotal -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case ActionTypes.COMMENT_UPDATE:
|
case ActionTypes.COMMENT_UPDATE:
|
||||||
Comment.withId(payload.id).update(payload.data);
|
Comment.withId(payload.id).update(payload.data);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.COMMENT_DELETE:
|
case ActionTypes.COMMENT_UPDATE__SUCCESS:
|
||||||
Comment.withId(payload.id).delete();
|
case ActionTypes.COMMENT_UPDATE_HANDLE:
|
||||||
|
Comment.upsert(payload.comment);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case ActionTypes.COMMENT_DELETE__SUCCESS:
|
case ActionTypes.COMMENT_DELETE: {
|
||||||
case ActionTypes.COMMENT_DELETE_HANDLE: {
|
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);
|
const commentModel = Comment.withId(payload.comment.id);
|
||||||
|
|
||||||
if (commentModel) {
|
if (commentModel) {
|
||||||
|
@ -75,6 +91,19 @@ export default class extends BaseModel {
|
||||||
|
|
||||||
break;
|
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:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,23 +28,7 @@ export const makeSelectCommentById = () =>
|
||||||
|
|
||||||
export const selectCommentById = 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 {
|
export default {
|
||||||
makeSelectCommentById,
|
makeSelectCommentById,
|
||||||
selectCommentById,
|
selectCommentById,
|
||||||
makeSelectCommentsTotalByCardId,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,25 @@ const defaultFind = (criteria, { limit } = {}) =>
|
||||||
|
|
||||||
/* Query methods */
|
/* 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);
|
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 });
|
const updateOne = (criteria, values) => Comment.updateOne(criteria).set({ ...values });
|
||||||
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// 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 = {
|
module.exports = {
|
||||||
createOne,
|
createOne,
|
||||||
|
|
|
@ -48,6 +48,11 @@ module.exports = {
|
||||||
stopwatch: {
|
stopwatch: {
|
||||||
type: 'json',
|
type: 'json',
|
||||||
},
|
},
|
||||||
|
commentsTotal: {
|
||||||
|
type: 'number',
|
||||||
|
defaultsTo: 0,
|
||||||
|
columnName: 'comments_total',
|
||||||
|
},
|
||||||
listChangedAt: {
|
listChangedAt: {
|
||||||
type: 'ref',
|
type: 'ref',
|
||||||
columnName: 'list_changed_at',
|
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