mirror of
https://github.com/wekan/wekan.git
synced 2026-01-26 19:26:09 +01:00
Run database migrations when opening board. Not when updating WeKan.
Thanks to xet7 !
This commit is contained in:
parent
ea30612013
commit
2b5c56484a
20 changed files with 2508 additions and 118 deletions
|
|
@ -4,3 +4,11 @@ if ('serviceWorker' in navigator) {
|
|||
navigator.serviceWorker.register('/pwa-service-worker.js');
|
||||
});
|
||||
}
|
||||
|
||||
// Import board converter for on-demand conversion
|
||||
import '/imports/lib/boardConverter';
|
||||
import '/imports/components/boardConversionProgress';
|
||||
|
||||
// Import migration manager and progress UI
|
||||
import '/imports/lib/migrationManager';
|
||||
import '/imports/components/migrationProgress';
|
||||
|
|
|
|||
184
client/components/boardConversionProgress.css
Normal file
184
client/components/boardConversionProgress.css
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/* Board Conversion Progress Styles */
|
||||
.board-conversion-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.board-conversion-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.board-conversion-modal {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.board-conversion-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.board-conversion-header p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.board-conversion-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.conversion-progress {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #2196F3, #21CBF3);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #2196F3;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.conversion-status i {
|
||||
margin-right: 8px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.conversion-time {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
background-color: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.conversion-time i {
|
||||
margin-right: 6px;
|
||||
color: #FF9800;
|
||||
}
|
||||
|
||||
.board-conversion-footer {
|
||||
padding: 16px 24px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.conversion-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.conversion-info i {
|
||||
margin-right: 6px;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 600px) {
|
||||
.board-conversion-modal {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.board-conversion-header,
|
||||
.board-conversion-content,
|
||||
.board-conversion-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
27
client/components/boardConversionProgress.jade
Normal file
27
client/components/boardConversionProgress.jade
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
template(name="boardConversionProgress")
|
||||
.board-conversion-overlay(class="{{#if isConverting}}active{{/if}}")
|
||||
.board-conversion-modal
|
||||
.board-conversion-header
|
||||
h3
|
||||
i.fa.fa-cogs
|
||||
| {{_ 'converting-board'}}
|
||||
p {{_ 'converting-board-description'}}
|
||||
|
||||
.board-conversion-content
|
||||
.conversion-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{conversionProgress}}%")
|
||||
.progress-text {{conversionProgress}}%
|
||||
|
||||
.conversion-status
|
||||
i.fa.fa-spinner.fa-spin
|
||||
| {{conversionStatus}}
|
||||
|
||||
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
|
||||
|
||||
.board-conversion-footer
|
||||
.conversion-info
|
||||
i.fa.fa-info-circle
|
||||
| {{_ 'conversion-info-text'}}
|
||||
31
client/components/boardConversionProgress.js
Normal file
31
client/components/boardConversionProgress.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { boardConverter } from '/imports/lib/boardConverter';
|
||||
|
||||
Template.boardConversionProgress.helpers({
|
||||
isConverting() {
|
||||
return boardConverter.isConverting.get();
|
||||
},
|
||||
|
||||
conversionProgress() {
|
||||
return boardConverter.conversionProgress.get();
|
||||
},
|
||||
|
||||
conversionStatus() {
|
||||
return boardConverter.conversionStatus.get();
|
||||
},
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return boardConverter.conversionEstimatedTime.get();
|
||||
}
|
||||
});
|
||||
|
||||
Template.boardConversionProgress.onCreated(function() {
|
||||
// Subscribe to conversion state changes
|
||||
this.autorun(() => {
|
||||
boardConverter.isConverting.get();
|
||||
boardConverter.conversionProgress.get();
|
||||
boardConverter.conversionStatus.get();
|
||||
boardConverter.conversionEstimatedTime.get();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
template(name="board")
|
||||
if isBoardReady.get
|
||||
if isMigrating
|
||||
+migrationProgress
|
||||
else if isConverting
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
if currentBoard
|
||||
if onlyShowCurrentCard
|
||||
+cardDetails(currentCard)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import dragscroll from '@wekanteam/dragscroll';
|
||||
import { boardConverter } from '/imports/lib/boardConverter';
|
||||
import { migrationManager } from '/imports/lib/migrationManager';
|
||||
|
||||
const subManager = new SubsManager();
|
||||
const { calculateIndex } = Utils;
|
||||
|
|
@ -9,6 +11,8 @@ const swimlaneWhileSortingHeight = 150;
|
|||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.isBoardReady = new ReactiveVar(false);
|
||||
this.isConverting = new ReactiveVar(false);
|
||||
this.isMigrating = new ReactiveVar(false);
|
||||
|
||||
// The pattern we use to manually handle data loading is described here:
|
||||
// https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
|
||||
|
|
@ -20,12 +24,49 @@ BlazeComponent.extendComponent({
|
|||
const handle = subManager.subscribe('board', currentBoardId, false);
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
this.isBoardReady.set(handle.ready());
|
||||
if (handle.ready()) {
|
||||
// Check if board needs conversion
|
||||
this.checkAndConvertBoard(currentBoardId);
|
||||
} else {
|
||||
this.isBoardReady.set(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async checkAndConvertBoard(boardId) {
|
||||
try {
|
||||
// First check if migrations need to be run
|
||||
if (migrationManager.needsMigration()) {
|
||||
this.isMigrating.set(true);
|
||||
await migrationManager.startMigration();
|
||||
this.isMigrating.set(false);
|
||||
}
|
||||
|
||||
// Then check if board needs conversion
|
||||
if (boardConverter.needsConversion(boardId)) {
|
||||
this.isConverting.set(true);
|
||||
const success = await boardConverter.convertBoard(boardId);
|
||||
this.isConverting.set(false);
|
||||
|
||||
if (success) {
|
||||
this.isBoardReady.set(true);
|
||||
} else {
|
||||
console.error('Board conversion failed');
|
||||
this.isBoardReady.set(true); // Still show board even if conversion failed
|
||||
}
|
||||
} else {
|
||||
this.isBoardReady.set(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during board conversion check:', error);
|
||||
this.isConverting.set(false);
|
||||
this.isMigrating.set(false);
|
||||
this.isBoardReady.set(true); // Show board even if conversion check failed
|
||||
}
|
||||
},
|
||||
|
||||
onlyShowCurrentCard() {
|
||||
return Utils.isMiniScreen() && Utils.getCurrentCardId(true);
|
||||
},
|
||||
|
|
@ -33,6 +74,14 @@ BlazeComponent.extendComponent({
|
|||
goHome() {
|
||||
FlowRouter.go('home');
|
||||
},
|
||||
|
||||
isConverting() {
|
||||
return this.isConverting.get();
|
||||
},
|
||||
|
||||
isMigrating() {
|
||||
return this.isMigrating.get();
|
||||
},
|
||||
}).register('board');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ template(name="defaultLayout")
|
|||
| {{{afterBodyStart}}}
|
||||
+Template.dynamic(template=content)
|
||||
| {{{beforeBodyEnd}}}
|
||||
+migrationProgress
|
||||
+boardConversionProgress
|
||||
if (Modal.isOpen)
|
||||
#modal
|
||||
.overlay
|
||||
|
|
|
|||
372
client/components/migrationProgress.css
Normal file
372
client/components/migrationProgress.css
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
/* Migration Progress Styles */
|
||||
.migration-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.migration-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.migration-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
max-width: 800px;
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
animation: slideInScale 0.4s ease-out;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
@keyframes slideInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.migration-header {
|
||||
padding: 24px 32px 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.migration-header h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.migration-header h3 i {
|
||||
margin-right: 12px;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.migration-header p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.migration-content {
|
||||
padding: 24px 32px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.migration-overview {
|
||||
margin-bottom: 32px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.overall-progress {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 6px;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.4),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.current-step {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.current-step i {
|
||||
margin-right: 8px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.estimated-time {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
background-color: #fff3cd;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.estimated-time i {
|
||||
margin-right: 6px;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.migration-steps {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.migration-steps h4 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.steps-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.migration-step {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.migration-step:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.migration-step.completed {
|
||||
background-color: #d4edda;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.migration-step.current {
|
||||
background-color: #cce7ff;
|
||||
border-left: 4px solid #667eea;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-icon i.fa-check-circle {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.step-icon i.fa-cog.fa-spin {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.step-icon i.fa-circle-o {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.step-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.step-description {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
text-align: right;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.step-progress .progress-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.step-progress-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.step-progress-bar .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.migration-status {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
background-color: #e3f2fd;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #bbdefb;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.migration-status i {
|
||||
margin-right: 8px;
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.migration-footer {
|
||||
padding: 16px 32px 24px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.migration-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.migration-info i {
|
||||
margin-right: 6px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.migration-warning {
|
||||
text-align: center;
|
||||
color: #856404;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
background-color: #fff3cd;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.migration-warning i {
|
||||
margin-right: 6px;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.migration-modal {
|
||||
width: 98%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.migration-header,
|
||||
.migration-content,
|
||||
.migration-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.migration-header h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
text-align: left;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.steps-list {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
63
client/components/migrationProgress.jade
Normal file
63
client/components/migrationProgress.jade
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
template(name="migrationProgress")
|
||||
.migration-overlay(class="{{#if isMigrating}}active{{/if}}")
|
||||
.migration-modal
|
||||
.migration-header
|
||||
h3
|
||||
i.fa.fa-database
|
||||
| {{_ 'database-migration'}}
|
||||
p {{_ 'database-migration-description'}}
|
||||
|
||||
.migration-content
|
||||
.migration-overview
|
||||
.overall-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{migrationProgress}}%")
|
||||
.progress-text {{migrationProgress}}%
|
||||
.progress-label {{_ 'overall-progress'}}
|
||||
|
||||
.current-step
|
||||
i.fa.fa-cog.fa-spin
|
||||
| {{migrationCurrentStep}}
|
||||
|
||||
.estimated-time(style="{{#unless migrationEstimatedTime}}display: none;{{/unless}}")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'estimated-time-remaining'}}: {{migrationEstimatedTime}}
|
||||
|
||||
.migration-steps
|
||||
h4 {{_ 'migration-steps'}}
|
||||
.steps-list
|
||||
each migrationSteps
|
||||
.migration-step(class="{{#if completed}}completed{{/if}}" class="{{#if isCurrentStep}}current{{/if}}")
|
||||
.step-header
|
||||
.step-icon
|
||||
if completed
|
||||
i.fa.fa-check-circle
|
||||
else if isCurrentStep
|
||||
i.fa.fa-cog.fa-spin
|
||||
else
|
||||
i.fa.fa-circle-o
|
||||
.step-info
|
||||
.step-name {{name}}
|
||||
.step-description {{description}}
|
||||
.step-progress
|
||||
if completed
|
||||
.progress-text 100%
|
||||
else if isCurrentStep
|
||||
.progress-text {{progress}}%
|
||||
else
|
||||
.progress-text 0%
|
||||
if isCurrentStep
|
||||
.step-progress-bar
|
||||
.progress-fill(style="width: {{progress}}%")
|
||||
|
||||
.migration-status
|
||||
i.fa.fa-info-circle
|
||||
| {{migrationStatus}}
|
||||
|
||||
.migration-footer
|
||||
.migration-info
|
||||
i.fa.fa-lightbulb-o
|
||||
| {{_ 'migration-info-text'}}
|
||||
.migration-warning
|
||||
i.fa.fa-exclamation-triangle
|
||||
| {{_ 'migration-warning-text'}}
|
||||
46
client/components/migrationProgress.js
Normal file
46
client/components/migrationProgress.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { Template } from 'meteor/templating';
|
||||
import { migrationManager } from '/imports/lib/migrationManager';
|
||||
|
||||
Template.migrationProgress.helpers({
|
||||
isMigrating() {
|
||||
return migrationManager.isMigrating.get();
|
||||
},
|
||||
|
||||
migrationProgress() {
|
||||
return migrationManager.migrationProgress.get();
|
||||
},
|
||||
|
||||
migrationStatus() {
|
||||
return migrationManager.migrationStatus.get();
|
||||
},
|
||||
|
||||
migrationCurrentStep() {
|
||||
return migrationManager.migrationCurrentStep.get();
|
||||
},
|
||||
|
||||
migrationEstimatedTime() {
|
||||
return migrationManager.migrationEstimatedTime.get();
|
||||
},
|
||||
|
||||
migrationSteps() {
|
||||
const steps = migrationManager.migrationSteps.get();
|
||||
const currentStep = migrationManager.migrationCurrentStep.get();
|
||||
|
||||
return steps.map(step => ({
|
||||
...step,
|
||||
isCurrentStep: step.name === currentStep
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
Template.migrationProgress.onCreated(function() {
|
||||
// Subscribe to migration state changes
|
||||
this.autorun(() => {
|
||||
migrationManager.isMigrating.get();
|
||||
migrationManager.migrationProgress.get();
|
||||
migrationManager.migrationStatus.get();
|
||||
migrationManager.migrationCurrentStep.get();
|
||||
migrationManager.migrationEstimatedTime.get();
|
||||
migrationManager.migrationSteps.get();
|
||||
});
|
||||
});
|
||||
203
client/lib/boardConverter.js
Normal file
203
client/lib/boardConverter.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
/**
|
||||
* Board Conversion Service
|
||||
* Handles conversion of boards from old database structure to new structure
|
||||
* without running migrations that could cause downtime
|
||||
*/
|
||||
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/lib/reactiveCache';
|
||||
|
||||
// Reactive variables for conversion progress
|
||||
export const conversionProgress = new ReactiveVar(0);
|
||||
export const conversionStatus = new ReactiveVar('');
|
||||
export const conversionEstimatedTime = new ReactiveVar('');
|
||||
export const isConverting = new ReactiveVar(false);
|
||||
|
||||
class BoardConverter {
|
||||
constructor() {
|
||||
this.conversionCache = new Map(); // Cache converted board IDs
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a board needs conversion
|
||||
* @param {string} boardId - The board ID to check
|
||||
* @returns {boolean} - True if board needs conversion
|
||||
*/
|
||||
needsConversion(boardId) {
|
||||
if (this.conversionCache.has(boardId)) {
|
||||
return false; // Already converted
|
||||
}
|
||||
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) return false;
|
||||
|
||||
// Check if any lists in this board don't have swimlaneId
|
||||
const lists = ReactiveCache.getLists({
|
||||
boardId: boardId,
|
||||
$or: [
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
]
|
||||
});
|
||||
|
||||
return lists.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking if board needs conversion:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a board from old structure to new structure
|
||||
* @param {string} boardId - The board ID to convert
|
||||
* @returns {Promise<boolean>} - True if conversion was successful
|
||||
*/
|
||||
async convertBoard(boardId) {
|
||||
if (this.conversionCache.has(boardId)) {
|
||||
return true; // Already converted
|
||||
}
|
||||
|
||||
isConverting.set(true);
|
||||
conversionProgress.set(0);
|
||||
conversionStatus.set('Starting board conversion...');
|
||||
|
||||
try {
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Error('Board not found');
|
||||
}
|
||||
|
||||
// Get the default swimlane for this board
|
||||
const defaultSwimlane = board.getDefaultSwimline();
|
||||
if (!defaultSwimlane) {
|
||||
throw new Error('No default swimlane found for board');
|
||||
}
|
||||
|
||||
// Get all lists that need conversion
|
||||
const listsToConvert = ReactiveCache.getLists({
|
||||
boardId: boardId,
|
||||
$or: [
|
||||
{ swimlaneId: { $exists: false } },
|
||||
{ swimlaneId: '' },
|
||||
{ swimlaneId: null }
|
||||
]
|
||||
});
|
||||
|
||||
if (listsToConvert.length === 0) {
|
||||
this.conversionCache.set(boardId, true);
|
||||
isConverting.set(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
conversionStatus.set(`Converting ${listsToConvert.length} lists...`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const totalLists = listsToConvert.length;
|
||||
let convertedLists = 0;
|
||||
|
||||
// Convert lists in batches to avoid blocking the UI
|
||||
const batchSize = 10;
|
||||
for (let i = 0; i < listsToConvert.length; i += batchSize) {
|
||||
const batch = listsToConvert.slice(i, i + batchSize);
|
||||
|
||||
// Process batch
|
||||
await this.processBatch(batch, defaultSwimlane._id);
|
||||
|
||||
convertedLists += batch.length;
|
||||
const progress = Math.round((convertedLists / totalLists) * 100);
|
||||
conversionProgress.set(progress);
|
||||
|
||||
// Calculate estimated time remaining
|
||||
const elapsed = Date.now() - startTime;
|
||||
const rate = convertedLists / elapsed; // lists per millisecond
|
||||
const remaining = totalLists - convertedLists;
|
||||
const estimatedMs = remaining / rate;
|
||||
|
||||
conversionStatus.set(`Converting list ${convertedLists} of ${totalLists}...`);
|
||||
conversionEstimatedTime.set(this.formatTime(estimatedMs));
|
||||
|
||||
// Allow UI to update
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
// Mark as converted
|
||||
this.conversionCache.set(boardId, true);
|
||||
|
||||
conversionStatus.set('Board conversion completed!');
|
||||
conversionProgress.set(100);
|
||||
|
||||
// Clear status after a delay
|
||||
setTimeout(() => {
|
||||
isConverting.set(false);
|
||||
conversionStatus.set('');
|
||||
conversionProgress.set(0);
|
||||
conversionEstimatedTime.set('');
|
||||
}, 2000);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error converting board:', error);
|
||||
conversionStatus.set(`Conversion failed: ${error.message}`);
|
||||
isConverting.set(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of lists for conversion
|
||||
* @param {Array} batch - Array of lists to convert
|
||||
* @param {string} defaultSwimlaneId - Default swimlane ID
|
||||
*/
|
||||
async processBatch(batch, defaultSwimlaneId) {
|
||||
const updates = batch.map(list => ({
|
||||
_id: list._id,
|
||||
swimlaneId: defaultSwimlaneId
|
||||
}));
|
||||
|
||||
// Update lists in batch
|
||||
updates.forEach(update => {
|
||||
ReactiveCache.getCollection('lists').update(update._id, {
|
||||
$set: { swimlaneId: update.swimlaneId }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in milliseconds to human readable format
|
||||
* @param {number} ms - Time in milliseconds
|
||||
* @returns {string} - Formatted time string
|
||||
*/
|
||||
formatTime(ms) {
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
const remainingMinutes = minutes % 60;
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
|
||||
} else if (minutes > 0) {
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear conversion cache (useful for testing)
|
||||
*/
|
||||
clearCache() {
|
||||
this.conversionCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const boardConverter = new BoardConverter();
|
||||
775
client/lib/migrationManager.js
Normal file
775
client/lib/migrationManager.js
Normal file
|
|
@ -0,0 +1,775 @@
|
|||
/**
|
||||
* Migration Manager
|
||||
* Handles all database migrations as steps during board loading
|
||||
* with detailed progress tracking and background persistence
|
||||
*/
|
||||
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { ReactiveCache } from '/imports/lib/reactiveCache';
|
||||
|
||||
// Reactive variables for migration progress
|
||||
export const migrationProgress = new ReactiveVar(0);
|
||||
export const migrationStatus = new ReactiveVar('');
|
||||
export const migrationCurrentStep = new ReactiveVar('');
|
||||
export const migrationSteps = new ReactiveVar([]);
|
||||
export const isMigrating = new ReactiveVar(false);
|
||||
export const migrationEstimatedTime = new ReactiveVar('');
|
||||
|
||||
class MigrationManager {
|
||||
constructor() {
|
||||
this.migrationCache = new Map(); // Cache completed migrations
|
||||
this.steps = this.initializeMigrationSteps();
|
||||
this.currentStepIndex = 0;
|
||||
this.startTime = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all migration steps with their details
|
||||
*/
|
||||
initializeMigrationSteps() {
|
||||
return [
|
||||
{
|
||||
id: 'board-background-color',
|
||||
name: 'Board Background Colors',
|
||||
description: 'Setting up board background colors',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-cardcounterlist-allowed',
|
||||
name: 'Card Counter List Settings',
|
||||
description: 'Adding card counter list permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-boardmemberlist-allowed',
|
||||
name: 'Board Member List Settings',
|
||||
description: 'Adding board member list permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'lowercase-board-permission',
|
||||
name: 'Board Permission Standardization',
|
||||
description: 'Converting board permissions to lowercase',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'change-attachments-type-for-non-images',
|
||||
name: 'Attachment Type Standardization',
|
||||
description: 'Updating attachment types for non-images',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'card-covers',
|
||||
name: 'Card Covers System',
|
||||
description: 'Setting up card cover functionality',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'use-css-class-for-boards-colors',
|
||||
name: 'Board Color CSS Classes',
|
||||
description: 'Converting board colors to CSS classes',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'denormalize-star-number-per-board',
|
||||
name: 'Board Star Counts',
|
||||
description: 'Calculating star counts per board',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-member-isactive-field',
|
||||
name: 'Member Activity Status',
|
||||
description: 'Adding member activity tracking',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-sort-checklists',
|
||||
name: 'Checklist Sorting',
|
||||
description: 'Adding sort order to checklists',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-swimlanes',
|
||||
name: 'Swimlanes System',
|
||||
description: 'Setting up swimlanes functionality',
|
||||
weight: 4,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-views',
|
||||
name: 'Board Views',
|
||||
description: 'Adding board view options',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-checklist-items',
|
||||
name: 'Checklist Items',
|
||||
description: 'Setting up checklist items system',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-card-types',
|
||||
name: 'Card Types',
|
||||
description: 'Adding card type functionality',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-custom-fields-to-cards',
|
||||
name: 'Custom Fields',
|
||||
description: 'Adding custom fields to cards',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-requester-field',
|
||||
name: 'Requester Field',
|
||||
description: 'Adding requester field to cards',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assigner-field',
|
||||
name: 'Assigner Field',
|
||||
description: 'Adding assigner field to cards',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-parent-field-to-cards',
|
||||
name: 'Card Parent Relationships',
|
||||
description: 'Adding parent field to cards',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-subtasks-boards',
|
||||
name: 'Subtasks Boards',
|
||||
description: 'Setting up subtasks board functionality',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-subtasks-sort',
|
||||
name: 'Subtasks Sorting',
|
||||
description: 'Adding sort order to subtasks',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-subtasks-allowed',
|
||||
name: 'Subtasks Permissions',
|
||||
description: 'Adding subtasks permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-authenticationMethod',
|
||||
name: 'Authentication Methods',
|
||||
description: 'Adding authentication method tracking',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-tag',
|
||||
name: 'Remove Tag Field',
|
||||
description: 'Removing deprecated tag field',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-customFields-references-broken',
|
||||
name: 'Fix Custom Fields References',
|
||||
description: 'Fixing broken custom field references',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-product-name',
|
||||
name: 'Product Name Settings',
|
||||
description: 'Adding product name configuration',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-logo',
|
||||
name: 'Hide Logo Setting',
|
||||
description: 'Adding hide logo option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-card-counter-list',
|
||||
name: 'Hide Card Counter Setting',
|
||||
description: 'Adding hide card counter option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-board-member-list',
|
||||
name: 'Hide Board Member List Setting',
|
||||
description: 'Adding hide board member list option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-displayAuthenticationMethod',
|
||||
name: 'Display Authentication Method',
|
||||
description: 'Adding authentication method display option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-defaultAuthenticationMethod',
|
||||
name: 'Default Authentication Method',
|
||||
description: 'Setting default authentication method',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-templates',
|
||||
name: 'Board Templates',
|
||||
description: 'Setting up board templates system',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'fix-circular-reference_',
|
||||
name: 'Fix Circular References',
|
||||
description: 'Fixing circular references in cards',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'mutate-boardIds-in-customfields',
|
||||
name: 'Custom Fields Board IDs',
|
||||
description: 'Updating board IDs in custom fields',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-missing-created-and-modified',
|
||||
name: 'Missing Timestamps',
|
||||
description: 'Adding missing created and modified timestamps',
|
||||
weight: 4,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'fix-incorrect-dates',
|
||||
name: 'Fix Incorrect Dates',
|
||||
description: 'Correcting incorrect date values',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assignee',
|
||||
name: 'Assignee Field',
|
||||
description: 'Adding assignee field to cards',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-profile-showDesktopDragHandles',
|
||||
name: 'Desktop Drag Handles',
|
||||
description: 'Adding desktop drag handles preference',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-profile-hiddenMinicardLabelText',
|
||||
name: 'Hidden Minicard Labels',
|
||||
description: 'Adding hidden minicard label text preference',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-receiveddate-allowed',
|
||||
name: 'Received Date Permissions',
|
||||
description: 'Adding received date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-startdate-allowed',
|
||||
name: 'Start Date Permissions',
|
||||
description: 'Adding start date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-duedate-allowed',
|
||||
name: 'Due Date Permissions',
|
||||
description: 'Adding due date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-enddate-allowed',
|
||||
name: 'End Date Permissions',
|
||||
description: 'Adding end date permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-members-allowed',
|
||||
name: 'Members Permissions',
|
||||
description: 'Adding members permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assignee-allowed',
|
||||
name: 'Assignee Permissions',
|
||||
description: 'Adding assignee permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-labels-allowed',
|
||||
name: 'Labels Permissions',
|
||||
description: 'Adding labels permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-checklists-allowed',
|
||||
name: 'Checklists Permissions',
|
||||
description: 'Adding checklists permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-attachments-allowed',
|
||||
name: 'Attachments Permissions',
|
||||
description: 'Adding attachments permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-comments-allowed',
|
||||
name: 'Comments Permissions',
|
||||
description: 'Adding comments permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-assigned-by-allowed',
|
||||
name: 'Assigned By Permissions',
|
||||
description: 'Adding assigned by permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-requested-by-allowed',
|
||||
name: 'Requested By Permissions',
|
||||
description: 'Adding requested by permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-activities-allowed',
|
||||
name: 'Activities Permissions',
|
||||
description: 'Adding activities permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-description-title-allowed',
|
||||
name: 'Description Title Permissions',
|
||||
description: 'Adding description title permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-description-text-allowed',
|
||||
name: 'Description Text Permissions',
|
||||
description: 'Adding description text permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-description-text-allowed-on-minicard',
|
||||
name: 'Minicard Description Permissions',
|
||||
description: 'Adding minicard description permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-sort-field-to-boards',
|
||||
name: 'Board Sort Field',
|
||||
description: 'Adding sort field to boards',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-default-profile-view',
|
||||
name: 'Default Profile View',
|
||||
description: 'Setting default profile view',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-logo-by-default',
|
||||
name: 'Hide Logo Default',
|
||||
description: 'Setting hide logo as default',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-card-counter-list-by-default',
|
||||
name: 'Hide Card Counter Default',
|
||||
description: 'Setting hide card counter as default',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-hide-board-member-list-by-default',
|
||||
name: 'Hide Board Member List Default',
|
||||
description: 'Setting hide board member list as default',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-card-number-allowed',
|
||||
name: 'Card Number Permissions',
|
||||
description: 'Adding card number permissions',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'assign-boardwise-card-numbers',
|
||||
name: 'Board Card Numbers',
|
||||
description: 'Assigning board-wise card numbers',
|
||||
weight: 3,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'add-card-details-show-lists',
|
||||
name: 'Card Details Show Lists',
|
||||
description: 'Adding card details show lists option',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-attachments-collectionFS-to-ostrioFiles',
|
||||
name: 'Migrate Attachments to Meteor-Files',
|
||||
description: 'Migrating attachments from CollectionFS to Meteor-Files',
|
||||
weight: 8,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-avatars-collectionFS-to-ostrioFiles',
|
||||
name: 'Migrate Avatars to Meteor-Files',
|
||||
description: 'Migrating avatars from CollectionFS to Meteor-Files',
|
||||
weight: 6,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-attachment-drop-index-cardId',
|
||||
name: 'Drop Attachment Index',
|
||||
description: 'Dropping old attachment index',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-attachment-migration-fix-source-import',
|
||||
name: 'Fix Attachment Source Import',
|
||||
description: 'Fixing attachment source import field',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'attachment-cardCopy-fix-boardId-etc',
|
||||
name: 'Fix Attachment Card Copy',
|
||||
description: 'Fixing attachment card copy board IDs',
|
||||
weight: 2,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-unused-planning-poker',
|
||||
name: 'Remove Planning Poker',
|
||||
description: 'Removing unused planning poker fields',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-user-profile-hiddenSystemMessages',
|
||||
name: 'Remove Hidden System Messages',
|
||||
description: 'Removing hidden system messages field',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'remove-user-profile-hideCheckedItems',
|
||||
name: 'Remove Hide Checked Items',
|
||||
description: 'Removing hide checked items field',
|
||||
weight: 1,
|
||||
completed: false,
|
||||
progress: 0
|
||||
},
|
||||
{
|
||||
id: 'migrate-lists-to-per-swimlane',
|
||||
name: 'Migrate Lists to Per-Swimlane',
|
||||
description: 'Migrating lists to per-swimlane structure',
|
||||
weight: 5,
|
||||
completed: false,
|
||||
progress: 0
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any migrations need to be run
|
||||
*/
|
||||
needsMigration() {
|
||||
// Check if any migration step is not completed
|
||||
return this.steps.some(step => !step.completed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total weight of all migrations
|
||||
*/
|
||||
getTotalWeight() {
|
||||
return this.steps.reduce((total, step) => total + step.weight, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completed weight
|
||||
*/
|
||||
getCompletedWeight() {
|
||||
return this.steps.reduce((total, step) => {
|
||||
return total + (step.completed ? step.weight : step.progress * step.weight / 100);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start migration process
|
||||
*/
|
||||
async startMigration() {
|
||||
if (isMigrating.get()) {
|
||||
return; // Already migrating
|
||||
}
|
||||
|
||||
isMigrating.set(true);
|
||||
migrationSteps.set([...this.steps]);
|
||||
this.startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Start server-side migration
|
||||
Meteor.call('migration.start', (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to start migration:', error);
|
||||
migrationStatus.set(`Migration failed: ${error.message}`);
|
||||
isMigrating.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Poll for progress updates
|
||||
this.pollMigrationProgress();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Migration failed:', error);
|
||||
migrationStatus.set(`Migration failed: ${error.message}`);
|
||||
isMigrating.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for migration progress updates
|
||||
*/
|
||||
pollMigrationProgress() {
|
||||
const pollInterval = setInterval(() => {
|
||||
Meteor.call('migration.getProgress', (error, result) => {
|
||||
if (error) {
|
||||
console.error('Failed to get migration progress:', error);
|
||||
clearInterval(pollInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
migrationProgress.set(result.progress);
|
||||
migrationStatus.set(result.status);
|
||||
migrationCurrentStep.set(result.currentStep);
|
||||
migrationSteps.set(result.steps);
|
||||
isMigrating.set(result.isMigrating);
|
||||
|
||||
// Update local steps
|
||||
if (result.steps) {
|
||||
this.steps = result.steps;
|
||||
}
|
||||
|
||||
// If migration is complete, stop polling
|
||||
if (!result.isMigrating && result.progress === 100) {
|
||||
clearInterval(pollInterval);
|
||||
|
||||
// Clear status after delay
|
||||
setTimeout(() => {
|
||||
migrationStatus.set('');
|
||||
migrationProgress.set(0);
|
||||
migrationEstimatedTime.set('');
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1000); // Poll every second
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single migration step
|
||||
*/
|
||||
async runMigrationStep(step) {
|
||||
// Simulate migration progress
|
||||
const steps = 10;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
step.progress = (i / steps) * 100;
|
||||
this.updateProgress();
|
||||
|
||||
// Simulate work
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
// In a real implementation, this would call the actual migration
|
||||
// For now, we'll simulate the migration
|
||||
console.log(`Running migration: ${step.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update progress variables
|
||||
*/
|
||||
updateProgress() {
|
||||
const totalWeight = this.getTotalWeight();
|
||||
const completedWeight = this.getCompletedWeight();
|
||||
const progress = Math.round((completedWeight / totalWeight) * 100);
|
||||
|
||||
migrationProgress.set(progress);
|
||||
migrationSteps.set([...this.steps]);
|
||||
|
||||
// Calculate estimated time remaining
|
||||
if (this.startTime && progress > 0) {
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
const rate = progress / elapsed; // progress per millisecond
|
||||
const remaining = 100 - progress;
|
||||
const estimatedMs = remaining / rate;
|
||||
migrationEstimatedTime.set(this.formatTime(estimatedMs));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in milliseconds to human readable format
|
||||
*/
|
||||
formatTime(ms) {
|
||||
if (ms < 1000) {
|
||||
return `${Math.round(ms)}ms`;
|
||||
}
|
||||
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
const remainingMinutes = minutes % 60;
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
|
||||
} else if (minutes > 0) {
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear migration cache (for testing)
|
||||
*/
|
||||
clearCache() {
|
||||
this.migrationCache.clear();
|
||||
this.steps.forEach(step => {
|
||||
step.completed = false;
|
||||
step.progress = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const migrationManager = new MigrationManager();
|
||||
Loading…
Add table
Add a link
Reference in a new issue