The Open Source kanban (built with Meteor). Keep variable/table/field names camelCase. For translations, only add Pull Request changes to wekan/i18n/en.i18n.json , other translations are done at https://transifex.com/wekan/wekan only.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
wekan/server/models/userPositionHistory.js

204 lines
5.6 KiB

import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Boards from '/models/boards';
import UserPositionHistory from '/models/userPositionHistory';
Meteor.startup(async () => {
await UserPositionHistory._collection.createIndexAsync({ userId: 1, boardId: 1, createdAt: -1 });
await UserPositionHistory._collection.createIndexAsync({ userId: 1, entityType: 1, entityId: 1 });
await UserPositionHistory._collection.createIndexAsync({ userId: 1, isCheckpoint: 1 });
await UserPositionHistory._collection.createIndexAsync({ batchId: 1 });
await UserPositionHistory._collection.createIndexAsync({ createdAt: 1 });
});
UserPositionHistory.trackChange = async function(options) {
const {
userId,
boardId,
entityType,
entityId,
actionType,
previousState,
newState,
batchId,
} = options;
if (!userId || !boardId || !entityType || !entityId || !actionType) {
throw new Meteor.Error('invalid-params', 'Missing required parameters');
}
const historyEntry = {
userId,
boardId,
entityType,
entityId,
actionType,
newState,
};
if (previousState) {
historyEntry.previousState = previousState;
historyEntry.previousSort = previousState.sort;
historyEntry.previousSwimlaneId = previousState.swimlaneId;
historyEntry.previousListId = previousState.listId;
historyEntry.previousBoardId = previousState.boardId;
}
if (newState) {
historyEntry.newSort = newState.sort;
historyEntry.newSwimlaneId = newState.swimlaneId;
historyEntry.newListId = newState.listId;
historyEntry.newBoardId = newState.boardId;
}
if (batchId) {
historyEntry.batchId = batchId;
}
return await UserPositionHistory.insertAsync(historyEntry);
};
UserPositionHistory.cleanup = async function() {
const users = await Meteor.users.find({}, { fields: { _id: 1 } }).fetchAsync();
for (const user of users) {
const boards = await Boards.find({ 'members.userId': user._id }, { fields: { _id: 1 } }).fetchAsync();
for (const board of boards) {
const history = await UserPositionHistory.find(
{ userId: user._id, boardId: board._id, isCheckpoint: { $ne: true } },
{ sort: { createdAt: -1 }, limit: 1000 },
).fetchAsync();
if (history.length >= 1000) {
const oldestToKeep = history[999].createdAt;
await UserPositionHistory.removeAsync({
userId: user._id,
boardId: board._id,
createdAt: { $lt: oldestToKeep },
isCheckpoint: { $ne: true },
});
}
}
}
};
if (Meteor.settings.public?.enableHistoryCleanup !== false) {
Meteor.setInterval(() => {
try {
UserPositionHistory.cleanup().catch(error => {
console.error('Error during history cleanup:', error);
});
} catch (e) {
console.error('Error during history cleanup:', e);
}
}, 24 * 60 * 60 * 1000);
}
Meteor.methods({
async 'userPositionHistory.createCheckpoint'(boardId, checkpointName) {
check(boardId, String);
check(checkpointName, String);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
return await UserPositionHistory.insertAsync({
userId: this.userId,
boardId,
entityType: 'checkpoint',
entityId: 'checkpoint',
actionType: 'create',
isCheckpoint: true,
checkpointName,
newState: {
timestamp: new Date(),
},
});
},
async 'userPositionHistory.undo'(historyId) {
check(historyId, String);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
const history = await UserPositionHistory.findOneAsync({ _id: historyId, userId: this.userId });
if (!history) {
throw new Meteor.Error('not-found', 'History entry not found');
}
return await history.undo();
},
async 'userPositionHistory.getRecent'(boardId, limit = 50) {
check(boardId, String);
check(limit, Number);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
return await UserPositionHistory.find(
{ userId: this.userId, boardId },
{ sort: { createdAt: -1 }, limit: Math.min(limit, 100) },
).fetchAsync();
},
async 'userPositionHistory.getCheckpoints'(boardId) {
check(boardId, String);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
return await UserPositionHistory.find(
{ userId: this.userId, boardId, isCheckpoint: true },
{ sort: { createdAt: -1 } },
).fetchAsync();
},
async 'userPositionHistory.restoreToCheckpoint'(checkpointId) {
check(checkpointId, String);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'Must be logged in');
}
const checkpoint = await UserPositionHistory.findOneAsync({
_id: checkpointId,
userId: this.userId,
isCheckpoint: true,
});
if (!checkpoint) {
throw new Meteor.Error('not-found', 'Checkpoint not found');
}
const changesToUndo = await UserPositionHistory.find(
{
userId: this.userId,
boardId: checkpoint.boardId,
createdAt: { $gt: checkpoint.createdAt },
isCheckpoint: { $ne: true },
},
{ sort: { createdAt: -1 } },
).fetchAsync();
let undoneCount = 0;
for (const change of changesToUndo) {
try {
if (await change.canUndo()) {
await change.undo();
undoneCount++;
}
} catch (e) {
console.warn('Failed to undo change:', change._id, e);
}
}
return { undoneCount, totalChanges: changesToUndo.length };
},
});