mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
Show original positions of swimlanes, lists and cards.
Thanks to xet7 ! Fixes #5939
This commit is contained in:
parent
915ab47a72
commit
2543df9425
13 changed files with 1719 additions and 0 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
170
models/positionHistory.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue