Show original positions of swimlanes, lists and cards.

Thanks to xet7 !

Fixes #5939
This commit is contained in:
Lauri Ojansivu 2025-10-16 20:23:05 +03:00
parent 915ab47a72
commit 2543df9425
13 changed files with 1719 additions and 0 deletions

View file

@ -8,6 +8,7 @@ import {
} from '../config/const';
import Attachments, { fileStoreStrategyFactory } from "./attachments";
import { copyFile } from './lib/fileStoreStrategy.js';
import PositionHistory from './positionHistory';
Cards = new Mongo.Collection('cards');
@ -3139,6 +3140,14 @@ if (Meteor.isServer) {
Cards.after.insert((userId, doc) => {
cardCreation(userId, doc);
// Track original position for new cards
Meteor.setTimeout(() => {
const card = Cards.findOne(doc._id);
if (card) {
card.trackOriginalPosition();
}
}, 100);
});
// New activity for card (un)archivage
Cards.after.update((userId, doc, fieldNames) => {
@ -4138,4 +4147,77 @@ JsonRoutes.add('GET', '/api/boards/:boardId/cards_count', function(
);
}
// Position history tracking methods
Cards.helpers({
/**
* Track the original position of this card
*/
trackOriginalPosition() {
const existingHistory = PositionHistory.findOne({
boardId: this.boardId,
entityType: 'card',
entityId: this._id,
});
if (!existingHistory) {
PositionHistory.insert({
boardId: this.boardId,
entityType: 'card',
entityId: this._id,
originalPosition: {
sort: this.sort,
title: this.title,
},
originalSwimlaneId: this.swimlaneId || null,
originalListId: this.listId || null,
originalTitle: this.title,
createdAt: new Date(),
updatedAt: new Date(),
});
}
},
/**
* Get the original position history for this card
*/
getOriginalPosition() {
return PositionHistory.findOne({
boardId: this.boardId,
entityType: 'card',
entityId: this._id,
});
},
/**
* Check if this card has moved from its original position
*/
hasMovedFromOriginalPosition() {
const history = this.getOriginalPosition();
if (!history) return false;
const currentSwimlaneId = this.swimlaneId || null;
const currentListId = this.listId || null;
return history.originalPosition.sort !== this.sort ||
history.originalSwimlaneId !== currentSwimlaneId ||
history.originalListId !== currentListId;
},
/**
* Get a description of the original position
*/
getOriginalPositionDescription() {
const history = this.getOriginalPosition();
if (!history) return 'No original position data';
const swimlaneInfo = history.originalSwimlaneId ?
` in swimlane ${history.originalSwimlaneId}` :
' in default swimlane';
const listInfo = history.originalListId ?
` in list ${history.originalListId}` :
'';
return `Original position: ${history.originalPosition.sort || 0}${swimlaneInfo}${listInfo}`;
},
});
export default Cards;

View file

@ -1,5 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ALLOWED_COLORS } from '/config/const';
import PositionHistory from './positionHistory';
Lists = new Mongo.Collection('lists');
@ -453,6 +454,14 @@ if (Meteor.isServer) {
// list is deleted
title: doc.title,
});
// Track original position for new lists
Meteor.setTimeout(() => {
const list = Lists.findOne(doc._id);
if (list) {
list.trackOriginalPosition();
}
}, 100);
});
Lists.before.remove((userId, doc) => {
@ -805,4 +814,77 @@ if (Meteor.isServer) {
});
}
// Position history tracking methods
Lists.helpers({
/**
* Track the original position of this list
*/
trackOriginalPosition() {
const existingHistory = PositionHistory.findOne({
boardId: this.boardId,
entityType: 'list',
entityId: this._id,
});
if (!existingHistory) {
PositionHistory.insert({
boardId: this.boardId,
entityType: 'list',
entityId: this._id,
originalPosition: {
sort: this.sort,
title: this.title,
},
originalSwimlaneId: this.swimlaneId || null,
originalTitle: this.title,
createdAt: new Date(),
updatedAt: new Date(),
});
}
},
/**
* Get the original position history for this list
*/
getOriginalPosition() {
return PositionHistory.findOne({
boardId: this.boardId,
entityType: 'list',
entityId: this._id,
});
},
/**
* Check if this list has moved from its original position
*/
hasMovedFromOriginalPosition() {
const history = this.getOriginalPosition();
if (!history) return false;
const currentSwimlaneId = this.swimlaneId || null;
return history.originalPosition.sort !== this.sort ||
history.originalSwimlaneId !== currentSwimlaneId;
},
/**
* Get a description of the original position
*/
getOriginalPositionDescription() {
const history = this.getOriginalPosition();
if (!history) return 'No original position data';
const swimlaneInfo = history.originalSwimlaneId ?
` in swimlane ${history.originalSwimlaneId}` :
' in default swimlane';
return `Original position: ${history.originalPosition.sort || 0}${swimlaneInfo}`;
},
/**
* Get the effective swimlane ID (for backward compatibility)
*/
getEffectiveSwimlaneId() {
return this.swimlaneId || null;
},
});
export default Lists;

170
models/positionHistory.js Normal file
View file

@ -0,0 +1,170 @@
import { ReactiveCache } from '/imports/reactiveCache';
/**
* PositionHistory collection to track original positions of swimlanes, lists, and cards
* before the list naming feature was added in commit 719ef87efceacfe91461a8eeca7cf74d11f4cc0a
*/
PositionHistory = new Mongo.Collection('positionHistory');
PositionHistory.attachSchema(
new SimpleSchema({
boardId: {
/**
* The board ID this position history belongs to
*/
type: String,
},
entityType: {
/**
* Type of entity: 'swimlane', 'list', or 'card'
*/
type: String,
allowedValues: ['swimlane', 'list', 'card'],
},
entityId: {
/**
* The ID of the entity (swimlane, list, or card)
*/
type: String,
},
originalPosition: {
/**
* The original position data before any changes
*/
type: Object,
blackbox: true,
},
originalSwimlaneId: {
/**
* The original swimlane ID (for lists and cards)
*/
type: String,
optional: true,
},
originalListId: {
/**
* The original list ID (for cards)
*/
type: String,
optional: true,
},
originalTitle: {
/**
* The original title before any changes
*/
type: String,
optional: true,
},
createdAt: {
/**
* When this position history was created
*/
type: Date,
autoValue() {
if (this.isInsert) {
return new Date();
} else if (this.isUpsert) {
return { $setOnInsert: new Date() };
} else {
this.unset();
}
},
},
updatedAt: {
/**
* When this position history was last updated
*/
type: Date,
autoValue() {
if (this.isUpdate || this.isUpsert || this.isInsert) {
return new Date();
} else {
this.unset();
}
},
},
}),
);
PositionHistory.helpers({
/**
* Get the original position data
*/
getOriginalPosition() {
return this.originalPosition;
},
/**
* Get the original title
*/
getOriginalTitle() {
return this.originalTitle || '';
},
/**
* Get the original swimlane ID
*/
getOriginalSwimlaneId() {
return this.originalSwimlaneId;
},
/**
* Get the original list ID
*/
getOriginalListId() {
return this.originalListId;
},
/**
* Check if this entity has been moved from its original position
*/
hasMoved() {
if (this.entityType === 'swimlane') {
return this.originalPosition.sort !== undefined;
} else if (this.entityType === 'list') {
return this.originalPosition.sort !== undefined ||
this.originalSwimlaneId !== this.entityId;
} else if (this.entityType === 'card') {
return this.originalPosition.sort !== undefined ||
this.originalSwimlaneId !== this.entityId ||
this.originalListId !== this.entityId;
}
return false;
},
/**
* Get a human-readable description of the original position
*/
getOriginalPositionDescription() {
const position = this.originalPosition;
if (!position) return 'Unknown position';
if (this.entityType === 'swimlane') {
return `Original position: ${position.sort || 0}`;
} else if (this.entityType === 'list') {
const swimlaneInfo = this.originalSwimlaneId ?
` in swimlane ${this.originalSwimlaneId}` :
' in default swimlane';
return `Original position: ${position.sort || 0}${swimlaneInfo}`;
} else if (this.entityType === 'card') {
const swimlaneInfo = this.originalSwimlaneId ?
` in swimlane ${this.originalSwimlaneId}` :
' in default swimlane';
const listInfo = this.originalListId ?
` in list ${this.originalListId}` :
'';
return `Original position: ${position.sort || 0}${swimlaneInfo}${listInfo}`;
}
return 'Unknown position';
}
});
if (Meteor.isServer) {
Meteor.startup(() => {
PositionHistory._collection.createIndex({ boardId: 1, entityType: 1, entityId: 1 });
PositionHistory._collection.createIndex({ boardId: 1, entityType: 1 });
PositionHistory._collection.createIndex({ createdAt: -1 });
});
}
export default PositionHistory;

View file

@ -1,5 +1,6 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { ALLOWED_COLORS } from '/config/const';
import PositionHistory from './positionHistory';
Swimlanes = new Mongo.Collection('swimlanes');
@ -366,6 +367,14 @@ if (Meteor.isServer) {
boardId: doc.boardId,
swimlaneId: doc._id,
});
// Track original position for new swimlanes
Meteor.setTimeout(() => {
const swimlane = Swimlanes.findOne(doc._id);
if (swimlane) {
swimlane.trackOriginalPosition();
}
}, 100);
});
Swimlanes.before.remove(function(userId, doc) {
@ -614,4 +623,64 @@ if (Meteor.isServer) {
);
}
// Position history tracking methods
Swimlanes.helpers({
/**
* Track the original position of this swimlane
*/
trackOriginalPosition() {
const existingHistory = PositionHistory.findOne({
boardId: this.boardId,
entityType: 'swimlane',
entityId: this._id,
});
if (!existingHistory) {
PositionHistory.insert({
boardId: this.boardId,
entityType: 'swimlane',
entityId: this._id,
originalPosition: {
sort: this.sort,
title: this.title,
},
originalTitle: this.title,
createdAt: new Date(),
updatedAt: new Date(),
});
}
},
/**
* Get the original position history for this swimlane
*/
getOriginalPosition() {
return PositionHistory.findOne({
boardId: this.boardId,
entityType: 'swimlane',
entityId: this._id,
});
},
/**
* Check if this swimlane has moved from its original position
*/
hasMovedFromOriginalPosition() {
const history = this.getOriginalPosition();
if (!history) return false;
return history.originalPosition.sort !== this.sort;
},
/**
* Get a description of the original position
*/
getOriginalPositionDescription() {
const history = this.getOriginalPosition();
if (!history) return 'No original position data';
return `Original position: ${history.originalPosition.sort || 0}`;
},
});
export default Swimlanes;