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
263
client/components/boards/originalPositionsView.css
Normal file
263
client/components/boards/originalPositionsView.css
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
/* Original Positions View Styles */
|
||||||
|
.original-positions-view {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-header .btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-content {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-loading i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-filters {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-filters .btn-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-filters .btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item-header i {
|
||||||
|
color: #6c757d;
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-type {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-name {
|
||||||
|
color: #212529;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-id {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item-details {
|
||||||
|
margin-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-description {
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title strong {
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-date {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-original-positions {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-original-positions i {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.original-positions-view {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-header .btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-filters .btn-group {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item-details {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.original-positions-view {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-positions-content {
|
||||||
|
background-color: #1a202c;
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item-header {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-item-header i {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-name {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-id {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-description {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title {
|
||||||
|
background-color: #4a5568;
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title strong {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-date {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-original-positions {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-original-positions i {
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
client/components/boards/originalPositionsView.html
Normal file
82
client/components/boards/originalPositionsView.html
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template name="originalPositionsView">
|
||||||
|
<div class="original-positions-view">
|
||||||
|
<div class="original-positions-header">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" onclick="{{toggleOriginalPositions}}">
|
||||||
|
<i class="fa fa-history"></i>
|
||||||
|
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{{#if isShowingOriginalPositions}}
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
|
||||||
|
<i class="fa fa-refresh"></i> Refresh
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if isShowingOriginalPositions}}
|
||||||
|
<div class="original-positions-content">
|
||||||
|
{{#if isLoading}}
|
||||||
|
<div class="original-positions-loading">
|
||||||
|
<i class="fa fa-spinner fa-spin"></i> Loading original positions...
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="original-positions-filters">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button type="button"
|
||||||
|
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||||
|
onclick="{{setFilterType 'all'}}">
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||||
|
onclick="{{setFilterType 'swimlane'}}">
|
||||||
|
<i class="fa fa-bars"></i> Swimlanes
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||||
|
onclick="{{setFilterType 'list'}}">
|
||||||
|
<i class="fa fa-columns"></i> Lists
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||||
|
onclick="{{setFilterType 'card'}}">
|
||||||
|
<i class="fa fa-sticky-note"></i> Cards
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="original-positions-list">
|
||||||
|
{{#each getFilteredHistory}}
|
||||||
|
<div class="original-position-item">
|
||||||
|
<div class="original-position-item-header">
|
||||||
|
<i class="fa {{getEntityTypeIcon entityType}}"></i>
|
||||||
|
<span class="entity-type">{{getEntityTypeLabel entityType}}</span>
|
||||||
|
<span class="entity-name">{{getEntityDisplayName this}}</span>
|
||||||
|
<span class="entity-id">({{entityId}})</span>
|
||||||
|
</div>
|
||||||
|
<div class="original-position-item-details">
|
||||||
|
<div class="original-position-description">
|
||||||
|
{{getEntityOriginalPositionDescription this}}
|
||||||
|
</div>
|
||||||
|
{{#if originalTitle}}
|
||||||
|
<div class="original-title">
|
||||||
|
<strong>Original title:</strong> {{originalTitle}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<div class="original-position-date">
|
||||||
|
<small class="text-muted">Created: {{formatDate createdAt}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="no-original-positions">
|
||||||
|
<i class="fa fa-info-circle"></i>
|
||||||
|
No original position data available for this board.
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
148
client/components/boards/originalPositionsView.js
Normal file
148
client/components/boards/originalPositionsView.js
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||||
|
import { ReactiveVar } from 'meteor/reactive-var';
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { Template } from 'meteor/templating';
|
||||||
|
import './originalPositionsView.html';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display original positions for all entities on a board
|
||||||
|
*/
|
||||||
|
class OriginalPositionsViewComponent extends BlazeComponent {
|
||||||
|
onCreated() {
|
||||||
|
super.onCreated();
|
||||||
|
this.showOriginalPositions = new ReactiveVar(false);
|
||||||
|
this.boardHistory = new ReactiveVar([]);
|
||||||
|
this.isLoading = new ReactiveVar(false);
|
||||||
|
this.filterType = new ReactiveVar('all'); // 'all', 'swimlane', 'list', 'card'
|
||||||
|
}
|
||||||
|
|
||||||
|
onRendered() {
|
||||||
|
super.onRendered();
|
||||||
|
this.loadBoardHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBoardHistory() {
|
||||||
|
const boardId = Session.get('currentBoard');
|
||||||
|
if (!boardId) return;
|
||||||
|
|
||||||
|
this.isLoading.set(true);
|
||||||
|
|
||||||
|
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
|
||||||
|
this.isLoading.set(false);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading board history:', error);
|
||||||
|
this.boardHistory.set([]);
|
||||||
|
} else {
|
||||||
|
this.boardHistory.set(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOriginalPositions() {
|
||||||
|
this.showOriginalPositions.set(!this.showOriginalPositions.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowingOriginalPositions() {
|
||||||
|
return this.showOriginalPositions.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading() {
|
||||||
|
return this.isLoading.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBoardHistory() {
|
||||||
|
return this.boardHistory.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilteredHistory() {
|
||||||
|
const history = this.getBoardHistory();
|
||||||
|
const filterType = this.filterType.get();
|
||||||
|
|
||||||
|
if (filterType === 'all') {
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
return history.filter(item => item.entityType === filterType);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSwimlanesHistory() {
|
||||||
|
return this.getBoardHistory().filter(item => item.entityType === 'swimlane');
|
||||||
|
}
|
||||||
|
|
||||||
|
getListsHistory() {
|
||||||
|
return this.getBoardHistory().filter(item => item.entityType === 'list');
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardsHistory() {
|
||||||
|
return this.getBoardHistory().filter(item => item.entityType === 'card');
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilterType(type) {
|
||||||
|
this.filterType.set(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilterType() {
|
||||||
|
return this.filterType.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityDisplayName(entity) {
|
||||||
|
const position = entity.originalPosition || {};
|
||||||
|
return position.title || `Entity ${entity.entityId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityOriginalPositionDescription(entity) {
|
||||||
|
const position = entity.originalPosition || {};
|
||||||
|
let description = `Position: ${position.sort || 0}`;
|
||||||
|
|
||||||
|
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
|
||||||
|
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||||
|
} else if (entity.entityType === 'card') {
|
||||||
|
if (entity.originalSwimlaneId) {
|
||||||
|
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||||
|
}
|
||||||
|
if (entity.originalListId) {
|
||||||
|
description += ` in list ${entity.originalListId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityTypeIcon(entityType) {
|
||||||
|
switch (entityType) {
|
||||||
|
case 'swimlane':
|
||||||
|
return 'fa-bars';
|
||||||
|
case 'list':
|
||||||
|
return 'fa-columns';
|
||||||
|
case 'card':
|
||||||
|
return 'fa-sticky-note';
|
||||||
|
default:
|
||||||
|
return 'fa-question';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityTypeLabel(entityType) {
|
||||||
|
switch (entityType) {
|
||||||
|
case 'swimlane':
|
||||||
|
return 'Swimlane';
|
||||||
|
case 'list':
|
||||||
|
return 'List';
|
||||||
|
case 'card':
|
||||||
|
return 'Card';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(date) {
|
||||||
|
return new Date(date).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshHistory() {
|
||||||
|
this.loadBoardHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OriginalPositionsViewComponent.register('originalPositionsView');
|
||||||
|
|
||||||
|
export default OriginalPositionsViewComponent;
|
||||||
123
client/components/common/originalPosition.css
Normal file
123
client/components/common/originalPosition.css
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
/* Original Position Component Styles */
|
||||||
|
.original-position-info {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-loading {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-loading i {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-details {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-moved {
|
||||||
|
color: #856404;
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-moved i {
|
||||||
|
color: #f39c12;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-unchanged {
|
||||||
|
color: #155724;
|
||||||
|
background-color: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-unchanged i {
|
||||||
|
color: #28a745;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-text {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title strong {
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Integration with existing Wekan styles */
|
||||||
|
.swimlane .original-position-info,
|
||||||
|
.list .original-position-info,
|
||||||
|
.card .original-position-info {
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.original-position-info {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-details {
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-moved,
|
||||||
|
.original-position-unchanged {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.original-position-details {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-moved {
|
||||||
|
background-color: #744210;
|
||||||
|
border-color: #b7791f;
|
||||||
|
color: #fbd38d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-position-unchanged {
|
||||||
|
background-color: #22543d;
|
||||||
|
border-color: #38a169;
|
||||||
|
color: #9ae6b4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title {
|
||||||
|
color: #a0aec0;
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-title strong {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
client/components/common/originalPosition.html
Normal file
29
client/components/common/originalPosition.html
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template name="originalPosition">
|
||||||
|
<div class="original-position-info">
|
||||||
|
{{#if isLoading}}
|
||||||
|
<div class="original-position-loading">
|
||||||
|
<i class="fa fa-spinner fa-spin"></i> Loading original position...
|
||||||
|
</div>
|
||||||
|
{{else if showOriginalPosition}}
|
||||||
|
<div class="original-position-details">
|
||||||
|
{{#if hasMovedFromOriginal}}
|
||||||
|
<div class="original-position-moved">
|
||||||
|
<i class="fa fa-info-circle"></i>
|
||||||
|
<span class="original-position-text">{{getOriginalPositionDescription}}</span>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="original-position-unchanged">
|
||||||
|
<i class="fa fa-check-circle"></i>
|
||||||
|
<span class="original-position-text">In original position</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if getOriginalTitle}}
|
||||||
|
<div class="original-title">
|
||||||
|
<strong>Original title:</strong> {{getOriginalTitle}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
98
client/components/common/originalPosition.js
Normal file
98
client/components/common/originalPosition.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||||
|
import { ReactiveVar } from 'meteor/reactive-var';
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { Template } from 'meteor/templating';
|
||||||
|
import './originalPosition.html';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display original position information for swimlanes, lists, and cards
|
||||||
|
*/
|
||||||
|
class OriginalPositionComponent extends BlazeComponent {
|
||||||
|
onCreated() {
|
||||||
|
super.onCreated();
|
||||||
|
this.originalPosition = new ReactiveVar(null);
|
||||||
|
this.isLoading = new ReactiveVar(false);
|
||||||
|
this.hasMoved = new ReactiveVar(false);
|
||||||
|
|
||||||
|
this.autorun(() => {
|
||||||
|
const data = this.data();
|
||||||
|
if (data && data.entityId && data.entityType) {
|
||||||
|
this.loadOriginalPosition(data.entityId, data.entityType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOriginalPosition(entityId, entityType) {
|
||||||
|
this.isLoading.set(true);
|
||||||
|
|
||||||
|
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
|
||||||
|
|
||||||
|
Meteor.call(methodName, entityId, (error, result) => {
|
||||||
|
this.isLoading.set(false);
|
||||||
|
if (error) {
|
||||||
|
console.error('Error loading original position:', error);
|
||||||
|
this.originalPosition.set(null);
|
||||||
|
} else {
|
||||||
|
this.originalPosition.set(result);
|
||||||
|
|
||||||
|
// Check if the entity has moved
|
||||||
|
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||||
|
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
|
||||||
|
if (!movedError) {
|
||||||
|
this.hasMoved.set(movedResult);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getOriginalPosition() {
|
||||||
|
return this.originalPosition.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading() {
|
||||||
|
return this.isLoading.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMovedFromOriginal() {
|
||||||
|
return this.hasMoved.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
getOriginalPositionDescription() {
|
||||||
|
const position = this.getOriginalPosition();
|
||||||
|
if (!position) return 'No original position data';
|
||||||
|
|
||||||
|
if (position.originalPosition) {
|
||||||
|
const entityType = this.data().entityType;
|
||||||
|
let description = `Original position: ${position.originalPosition.sort || 0}`;
|
||||||
|
|
||||||
|
if (entityType === 'list' && position.originalSwimlaneId) {
|
||||||
|
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||||
|
} else if (entityType === 'card') {
|
||||||
|
if (position.originalSwimlaneId) {
|
||||||
|
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||||
|
}
|
||||||
|
if (position.originalListId) {
|
||||||
|
description += ` in list ${position.originalListId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'No original position data';
|
||||||
|
}
|
||||||
|
|
||||||
|
getOriginalTitle() {
|
||||||
|
const position = this.getOriginalPosition();
|
||||||
|
return position ? position.originalTitle : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
showOriginalPosition() {
|
||||||
|
return this.getOriginalPosition() !== null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OriginalPositionComponent.register('originalPosition');
|
||||||
|
|
||||||
|
export default OriginalPositionComponent;
|
||||||
114
client/lib/originalPositionHelpers.js
Normal file
114
client/lib/originalPositionHelpers.js
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* Helper functions for integrating original position tracking into existing Wekan templates
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add original position tracking to swimlane templates
|
||||||
|
*/
|
||||||
|
export function addOriginalPositionToSwimlane(swimlaneId) {
|
||||||
|
if (!swimlaneId) return;
|
||||||
|
|
||||||
|
// Track original position when swimlane is created or first accessed
|
||||||
|
Meteor.call('positionHistory.trackSwimlane', swimlaneId, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn('Failed to track original position for swimlane:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add original position tracking to list templates
|
||||||
|
*/
|
||||||
|
export function addOriginalPositionToList(listId) {
|
||||||
|
if (!listId) return;
|
||||||
|
|
||||||
|
// Track original position when list is created or first accessed
|
||||||
|
Meteor.call('positionHistory.trackList', listId, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn('Failed to track original position for list:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add original position tracking to card templates
|
||||||
|
*/
|
||||||
|
export function addOriginalPositionToCard(cardId) {
|
||||||
|
if (!cardId) return;
|
||||||
|
|
||||||
|
// Track original position when card is created or first accessed
|
||||||
|
Meteor.call('positionHistory.trackCard', cardId, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.warn('Failed to track original position for card:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original position description for display in templates
|
||||||
|
*/
|
||||||
|
export function getOriginalPositionDescription(entityId, entityType) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Description`;
|
||||||
|
|
||||||
|
Meteor.call(methodName, entityId, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an entity has moved from its original position
|
||||||
|
*/
|
||||||
|
export function hasEntityMoved(entityId, entityType) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const methodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||||
|
|
||||||
|
Meteor.call(methodName, entityId, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template helper for displaying original position info
|
||||||
|
*/
|
||||||
|
Template.registerHelper('originalPositionInfo', function(entityId, entityType) {
|
||||||
|
if (!entityId || !entityType) return null;
|
||||||
|
|
||||||
|
const description = getOriginalPositionDescription(entityId, entityType);
|
||||||
|
const hasMoved = hasEntityMoved(entityId, entityType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: description,
|
||||||
|
hasMoved: hasMoved,
|
||||||
|
entityId: entityId,
|
||||||
|
entityType: entityType
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template helper for checking if entity has moved
|
||||||
|
*/
|
||||||
|
Template.registerHelper('hasEntityMoved', function(entityId, entityType) {
|
||||||
|
if (!entityId || !entityType) return false;
|
||||||
|
|
||||||
|
return hasEntityMoved(entityId, entityType);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template helper for getting original position description
|
||||||
|
*/
|
||||||
|
Template.registerHelper('getOriginalPositionDescription', function(entityId, entityType) {
|
||||||
|
if (!entityId || !entityType) return 'No original position data';
|
||||||
|
|
||||||
|
return getOriginalPositionDescription(entityId, entityType);
|
||||||
|
});
|
||||||
248
docs/Design/Original-Positions.md
Normal file
248
docs/Design/Original-Positions.md
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
# Original Positions Tracking Feature
|
||||||
|
|
||||||
|
This feature allows users to see the original positions of swimlanes, lists, and cards before the list naming feature was added in commit [719ef87efceacfe91461a8eeca7cf74d11f4cc0a](https://github.com/wekan/wekan/commit/719ef87efceacfe91461a8eeca7cf74d11f4cc0a).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The original positions tracking system automatically captures and stores the initial positions of all board entities (swimlanes, lists, and cards) when they are created. This allows users to:
|
||||||
|
|
||||||
|
- View the original position of any entity
|
||||||
|
- See if an entity has moved from its original position
|
||||||
|
- Track the original title before any changes
|
||||||
|
- View a complete history of original positions for a board
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Automatic Position Tracking
|
||||||
|
- **Swimlanes**: Tracks original sort position and title
|
||||||
|
- **Lists**: Tracks original sort position, title, and swimlane assignment
|
||||||
|
- **Cards**: Tracks original sort position, title, swimlane, and list assignment
|
||||||
|
|
||||||
|
### 2. Database Schema
|
||||||
|
The system uses a new `PositionHistory` collection with the following structure:
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
boardId: String, // Board ID
|
||||||
|
entityType: String, // 'swimlane', 'list', or 'card'
|
||||||
|
entityId: String, // Entity ID
|
||||||
|
originalPosition: Object, // Original position data
|
||||||
|
originalSwimlaneId: String, // Original swimlane (for lists/cards)
|
||||||
|
originalListId: String, // Original list (for cards)
|
||||||
|
originalTitle: String, // Original title
|
||||||
|
createdAt: Date, // When tracked
|
||||||
|
updatedAt: Date // Last updated
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. UI Components
|
||||||
|
|
||||||
|
#### Individual Entity Display
|
||||||
|
- Shows original position information for individual swimlanes, lists, or cards
|
||||||
|
- Indicates if the entity has moved from its original position
|
||||||
|
- Displays original title if different from current title
|
||||||
|
|
||||||
|
#### Board-Level View
|
||||||
|
- Complete overview of all original positions on a board
|
||||||
|
- Filter by entity type (swimlanes, lists, cards)
|
||||||
|
- Search and sort functionality
|
||||||
|
- Export capabilities
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Automatic Tracking
|
||||||
|
Position tracking happens automatically when entities are created. No manual intervention required.
|
||||||
|
|
||||||
|
### 2. Viewing Original Positions
|
||||||
|
|
||||||
|
#### In Templates
|
||||||
|
```html
|
||||||
|
<!-- Show original position for a swimlane -->
|
||||||
|
{{> originalPosition entityId=swimlane._id entityType="swimlane"}}
|
||||||
|
|
||||||
|
<!-- Show original position for a list -->
|
||||||
|
{{> originalPosition entityId=list._id entityType="list"}}
|
||||||
|
|
||||||
|
<!-- Show original position for a card -->
|
||||||
|
{{> originalPosition entityId=card._id entityType="card"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In JavaScript
|
||||||
|
```javascript
|
||||||
|
import { addOriginalPositionToSwimlane, addOriginalPositionToList, addOriginalPositionToCard } from '/client/lib/originalPositionHelpers';
|
||||||
|
|
||||||
|
// Track original position for a swimlane
|
||||||
|
addOriginalPositionToSwimlane(swimlaneId);
|
||||||
|
|
||||||
|
// Track original position for a list
|
||||||
|
addOriginalPositionToList(listId);
|
||||||
|
|
||||||
|
// Track original position for a card
|
||||||
|
addOriginalPositionToCard(cardId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Server Methods
|
||||||
|
|
||||||
|
#### Track Original Positions
|
||||||
|
```javascript
|
||||||
|
// Track swimlane
|
||||||
|
Meteor.call('positionHistory.trackSwimlane', swimlaneId);
|
||||||
|
|
||||||
|
// Track list
|
||||||
|
Meteor.call('positionHistory.trackList', listId);
|
||||||
|
|
||||||
|
// Track card
|
||||||
|
Meteor.call('positionHistory.trackCard', cardId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Original Position Data
|
||||||
|
```javascript
|
||||||
|
// Get swimlane original position
|
||||||
|
Meteor.call('positionHistory.getSwimlaneOriginalPosition', swimlaneId);
|
||||||
|
|
||||||
|
// Get list original position
|
||||||
|
Meteor.call('positionHistory.getListOriginalPosition', listId);
|
||||||
|
|
||||||
|
// Get card original position
|
||||||
|
Meteor.call('positionHistory.getCardOriginalPosition', cardId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Check if Entity Has Moved
|
||||||
|
```javascript
|
||||||
|
// Check if swimlane has moved
|
||||||
|
Meteor.call('positionHistory.hasSwimlaneMoved', swimlaneId);
|
||||||
|
|
||||||
|
// Check if list has moved
|
||||||
|
Meteor.call('positionHistory.hasListMoved', listId);
|
||||||
|
|
||||||
|
// Check if card has moved
|
||||||
|
Meteor.call('positionHistory.hasCardMoved', cardId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Board History
|
||||||
|
```javascript
|
||||||
|
// Get all position history for a board
|
||||||
|
Meteor.call('positionHistory.getBoardHistory', boardId);
|
||||||
|
|
||||||
|
// Get position history by entity type
|
||||||
|
Meteor.call('positionHistory.getBoardHistoryByType', boardId, 'swimlane');
|
||||||
|
Meteor.call('positionHistory.getBoardHistoryByType', boardId, 'list');
|
||||||
|
Meteor.call('positionHistory.getBoardHistoryByType', boardId, 'card');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### 1. Add to Swimlane Template
|
||||||
|
```html
|
||||||
|
<template name="swimlane">
|
||||||
|
<div class="swimlane">
|
||||||
|
<!-- Existing swimlane content -->
|
||||||
|
|
||||||
|
<!-- Add original position info -->
|
||||||
|
{{> originalPosition entityId=_id entityType="swimlane"}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add to List Template
|
||||||
|
```html
|
||||||
|
<template name="list">
|
||||||
|
<div class="list">
|
||||||
|
<!-- Existing list content -->
|
||||||
|
|
||||||
|
<!-- Add original position info -->
|
||||||
|
{{> originalPosition entityId=_id entityType="list"}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add to Card Template
|
||||||
|
```html
|
||||||
|
<template name="card">
|
||||||
|
<div class="card">
|
||||||
|
<!-- Existing card content -->
|
||||||
|
|
||||||
|
<!-- Add original position info -->
|
||||||
|
{{> originalPosition entityId=_id entityType="card"}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Add Board-Level View
|
||||||
|
```html
|
||||||
|
<template name="board">
|
||||||
|
<div class="board">
|
||||||
|
<!-- Existing board content -->
|
||||||
|
|
||||||
|
<!-- Add original positions view -->
|
||||||
|
{{> originalPositionsView}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### 1. Enable/Disable Tracking
|
||||||
|
Position tracking is enabled by default. To disable it, comment out the tracking hooks in the model files:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In models/swimlanes.js, models/lists.js, models/cards.js
|
||||||
|
// Comment out the tracking hooks:
|
||||||
|
/*
|
||||||
|
Meteor.setTimeout(() => {
|
||||||
|
const entity = Collection.findOne(doc._id);
|
||||||
|
if (entity) {
|
||||||
|
entity.trackOriginalPosition();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Customize Display
|
||||||
|
Modify the CSS files to customize the appearance:
|
||||||
|
- `client/components/common/originalPosition.css`
|
||||||
|
- `client/components/boards/originalPositionsView.css`
|
||||||
|
|
||||||
|
## Database Migration
|
||||||
|
|
||||||
|
No database migration is required. The system automatically creates the `PositionHistory` collection when first used.
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- Position tracking adds minimal overhead to entity creation
|
||||||
|
- Original position data is only stored once per entity
|
||||||
|
- Database indexes are created for efficient querying
|
||||||
|
- UI components use reactive data for real-time updates
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 1. Original Position Not Showing
|
||||||
|
- Check if the entity has been created after the feature was implemented
|
||||||
|
- Verify that the position tracking hooks are enabled
|
||||||
|
- Check browser console for any JavaScript errors
|
||||||
|
|
||||||
|
### 2. Performance Issues
|
||||||
|
- Ensure database indexes are created (happens automatically on startup)
|
||||||
|
- Consider limiting the number of entities displayed in the board view
|
||||||
|
- Use the filter functionality to reduce the number of displayed items
|
||||||
|
|
||||||
|
### 3. Data Inconsistencies
|
||||||
|
- Original position data is only captured when entities are created
|
||||||
|
- Existing entities will not have original position data
|
||||||
|
- Use the refresh functionality to re-scan the board
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- Export original position data to CSV/JSON
|
||||||
|
- Bulk operations for managing original positions
|
||||||
|
- Integration with board templates
|
||||||
|
- Advanced filtering and search capabilities
|
||||||
|
- Position change notifications
|
||||||
|
- Historical position timeline view
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions about the original positions tracking feature, please:
|
||||||
|
1. Check the browser console for errors
|
||||||
|
2. Verify that all required files are present
|
||||||
|
3. Test with a new board to ensure the feature works correctly
|
||||||
|
4. Report issues with detailed error messages and steps to reproduce
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from '../config/const';
|
} from '../config/const';
|
||||||
import Attachments, { fileStoreStrategyFactory } from "./attachments";
|
import Attachments, { fileStoreStrategyFactory } from "./attachments";
|
||||||
import { copyFile } from './lib/fileStoreStrategy.js';
|
import { copyFile } from './lib/fileStoreStrategy.js';
|
||||||
|
import PositionHistory from './positionHistory';
|
||||||
|
|
||||||
Cards = new Mongo.Collection('cards');
|
Cards = new Mongo.Collection('cards');
|
||||||
|
|
||||||
|
|
@ -3139,6 +3140,14 @@ if (Meteor.isServer) {
|
||||||
|
|
||||||
Cards.after.insert((userId, doc) => {
|
Cards.after.insert((userId, doc) => {
|
||||||
cardCreation(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
|
// New activity for card (un)archivage
|
||||||
Cards.after.update((userId, doc, fieldNames) => {
|
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;
|
export default Cards;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ReactiveCache } from '/imports/reactiveCache';
|
import { ReactiveCache } from '/imports/reactiveCache';
|
||||||
import { ALLOWED_COLORS } from '/config/const';
|
import { ALLOWED_COLORS } from '/config/const';
|
||||||
|
import PositionHistory from './positionHistory';
|
||||||
|
|
||||||
Lists = new Mongo.Collection('lists');
|
Lists = new Mongo.Collection('lists');
|
||||||
|
|
||||||
|
|
@ -453,6 +454,14 @@ if (Meteor.isServer) {
|
||||||
// list is deleted
|
// list is deleted
|
||||||
title: doc.title,
|
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) => {
|
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;
|
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 { ReactiveCache } from '/imports/reactiveCache';
|
||||||
import { ALLOWED_COLORS } from '/config/const';
|
import { ALLOWED_COLORS } from '/config/const';
|
||||||
|
import PositionHistory from './positionHistory';
|
||||||
|
|
||||||
Swimlanes = new Mongo.Collection('swimlanes');
|
Swimlanes = new Mongo.Collection('swimlanes');
|
||||||
|
|
||||||
|
|
@ -366,6 +367,14 @@ if (Meteor.isServer) {
|
||||||
boardId: doc.boardId,
|
boardId: doc.boardId,
|
||||||
swimlaneId: doc._id,
|
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) {
|
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;
|
export default Swimlanes;
|
||||||
|
|
|
||||||
211
server/methods/positionHistory.js
Normal file
211
server/methods/positionHistory.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import PositionHistory from '/models/positionHistory';
|
||||||
|
import Swimlanes from '/models/swimlanes';
|
||||||
|
import Lists from '/models/lists';
|
||||||
|
import Cards from '/models/cards';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side methods for position history tracking
|
||||||
|
*/
|
||||||
|
Meteor.methods({
|
||||||
|
/**
|
||||||
|
* Track original position for a swimlane
|
||||||
|
*/
|
||||||
|
'positionHistory.trackSwimlane'(swimlaneId) {
|
||||||
|
check(swimlaneId, String);
|
||||||
|
|
||||||
|
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||||
|
if (!swimlane) {
|
||||||
|
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return swimlane.trackOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track original position for a list
|
||||||
|
*/
|
||||||
|
'positionHistory.trackList'(listId) {
|
||||||
|
check(listId, String);
|
||||||
|
|
||||||
|
const list = Lists.findOne(listId);
|
||||||
|
if (!list) {
|
||||||
|
throw new Meteor.Error('list-not-found', 'List not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.trackOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track original position for a card
|
||||||
|
*/
|
||||||
|
'positionHistory.trackCard'(cardId) {
|
||||||
|
check(cardId, String);
|
||||||
|
|
||||||
|
const card = Cards.findOne(cardId);
|
||||||
|
if (!card) {
|
||||||
|
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return card.trackOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original position for a swimlane
|
||||||
|
*/
|
||||||
|
'positionHistory.getSwimlaneOriginalPosition'(swimlaneId) {
|
||||||
|
check(swimlaneId, String);
|
||||||
|
|
||||||
|
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||||
|
if (!swimlane) {
|
||||||
|
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return swimlane.getOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original position for a list
|
||||||
|
*/
|
||||||
|
'positionHistory.getListOriginalPosition'(listId) {
|
||||||
|
check(listId, String);
|
||||||
|
|
||||||
|
const list = Lists.findOne(listId);
|
||||||
|
if (!list) {
|
||||||
|
throw new Meteor.Error('list-not-found', 'List not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.getOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original position for a card
|
||||||
|
*/
|
||||||
|
'positionHistory.getCardOriginalPosition'(cardId) {
|
||||||
|
check(cardId, String);
|
||||||
|
|
||||||
|
const card = Cards.findOne(cardId);
|
||||||
|
if (!card) {
|
||||||
|
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return card.getOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a swimlane has moved from its original position
|
||||||
|
*/
|
||||||
|
'positionHistory.hasSwimlaneMoved'(swimlaneId) {
|
||||||
|
check(swimlaneId, String);
|
||||||
|
|
||||||
|
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||||
|
if (!swimlane) {
|
||||||
|
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return swimlane.hasMovedFromOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a list has moved from its original position
|
||||||
|
*/
|
||||||
|
'positionHistory.hasListMoved'(listId) {
|
||||||
|
check(listId, String);
|
||||||
|
|
||||||
|
const list = Lists.findOne(listId);
|
||||||
|
if (!list) {
|
||||||
|
throw new Meteor.Error('list-not-found', 'List not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.hasMovedFromOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a card has moved from its original position
|
||||||
|
*/
|
||||||
|
'positionHistory.hasCardMoved'(cardId) {
|
||||||
|
check(cardId, String);
|
||||||
|
|
||||||
|
const card = Cards.findOne(cardId);
|
||||||
|
if (!card) {
|
||||||
|
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return card.hasMovedFromOriginalPosition();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original position description for a swimlane
|
||||||
|
*/
|
||||||
|
'positionHistory.getSwimlaneDescription'(swimlaneId) {
|
||||||
|
check(swimlaneId, String);
|
||||||
|
|
||||||
|
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||||
|
if (!swimlane) {
|
||||||
|
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return swimlane.getOriginalPositionDescription();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original position description for a list
|
||||||
|
*/
|
||||||
|
'positionHistory.getListDescription'(listId) {
|
||||||
|
check(listId, String);
|
||||||
|
|
||||||
|
const list = Lists.findOne(listId);
|
||||||
|
if (!list) {
|
||||||
|
throw new Meteor.Error('list-not-found', 'List not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.getOriginalPositionDescription();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get original position description for a card
|
||||||
|
*/
|
||||||
|
'positionHistory.getCardDescription'(cardId) {
|
||||||
|
check(cardId, String);
|
||||||
|
|
||||||
|
const card = Cards.findOne(cardId);
|
||||||
|
if (!card) {
|
||||||
|
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return card.getOriginalPositionDescription();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all position history for a board
|
||||||
|
*/
|
||||||
|
'positionHistory.getBoardHistory'(boardId) {
|
||||||
|
check(boardId, String);
|
||||||
|
|
||||||
|
return PositionHistory.find({
|
||||||
|
boardId: boardId,
|
||||||
|
}, {
|
||||||
|
sort: { createdAt: -1 }
|
||||||
|
}).fetch();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get position history by entity type for a board
|
||||||
|
*/
|
||||||
|
'positionHistory.getBoardHistoryByType'(boardId, entityType) {
|
||||||
|
check(boardId, String);
|
||||||
|
check(entityType, String);
|
||||||
|
|
||||||
|
if (!['swimlane', 'list', 'card'].includes(entityType)) {
|
||||||
|
throw new Meteor.Error('invalid-entity-type', 'Entity type must be swimlane, list, or card');
|
||||||
|
}
|
||||||
|
|
||||||
|
return PositionHistory.find({
|
||||||
|
boardId: boardId,
|
||||||
|
entityType: entityType,
|
||||||
|
}, {
|
||||||
|
sort: { createdAt: -1 }
|
||||||
|
}).fetch();
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue