Fixed Admin Panel Settings menus Attachments and Cron.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu 2025-10-13 22:17:32 +03:00
parent e0013b9b63
commit 7bb1e24bda
8 changed files with 485 additions and 1302 deletions

View file

@ -15,6 +15,3 @@ import '/client/components/migrationProgress';
// Import cron settings
import '/client/components/settings/cronSettings';
// Import settings tabs CSS
import '/client/components/settings/settingsTabs.css';

View file

@ -1,29 +1,74 @@
template(name="attachmentSettings")
.attachment-settings-content
.settings-tabs
ul.tab-nav
li(class="{{#if showStorageSettings}}active{{/if}}")
a.js-attachment-storage-settings(data-id="storage-settings")
i.fa.fa-cog
| {{_ 'attachment-storage-settings'}}
li(class="{{#if showMigration}}active{{/if}}")
a.js-attachment-migration(data-id="attachment-migration")
i.fa.fa-arrow-right
| {{_ 'attachment-migration'}}
li(class="{{#if showMonitoring}}active{{/if}}")
a.js-attachment-monitoring(data-id="attachment-monitoring")
i.fa.fa-chart-line
| {{_ 'attachment-monitoring'}}
.tab-content
if loading
+spinner
else if showStorageSettings
+storageSettings
else if showMigration
+attachmentMigration
else if showMonitoring
+attachmentMonitoring
ul#attachment-setting.setting-detail
li
h3 {{_ 'attachment-storage-configuration'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
li
h3 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
li
h3 {{_ 's3-minio-storage'}}
.form-group
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
template(name="storageSettings")
.storage-settings

View file

@ -1,504 +0,0 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { Chart } from 'chart.js';
// Global reactive variables for attachment settings
const attachmentSettings = {
loading: new ReactiveVar(false),
showStorageSettings: new ReactiveVar(false),
showMigration: new ReactiveVar(false),
showMonitoring: new ReactiveVar(false),
// Storage configuration
filesystemPath: new ReactiveVar(''),
attachmentsPath: new ReactiveVar(''),
avatarsPath: new ReactiveVar(''),
gridfsEnabled: new ReactiveVar(false),
s3Enabled: new ReactiveVar(false),
s3Endpoint: new ReactiveVar(''),
s3Bucket: new ReactiveVar(''),
s3Region: new ReactiveVar(''),
s3SslEnabled: new ReactiveVar(false),
s3Port: new ReactiveVar(443),
// Migration settings
migrationBatchSize: new ReactiveVar(10),
migrationDelayMs: new ReactiveVar(1000),
migrationCpuThreshold: new ReactiveVar(70),
migrationProgress: new ReactiveVar(0),
migrationStatus: new ReactiveVar('idle'),
migrationLog: new ReactiveVar(''),
// Monitoring data
totalAttachments: new ReactiveVar(0),
filesystemAttachments: new ReactiveVar(0),
gridfsAttachments: new ReactiveVar(0),
s3Attachments: new ReactiveVar(0),
totalSize: new ReactiveVar(0),
filesystemSize: new ReactiveVar(0),
gridfsSize: new ReactiveVar(0),
s3Size: new ReactiveVar(0),
// Migration state
isMigrationRunning: new ReactiveVar(false),
isMigrationPaused: new ReactiveVar(false),
migrationQueue: new ReactiveVar([]),
currentMigration: new ReactiveVar(null)
};
// Main attachment settings component
BlazeComponent.extendComponent({
onCreated() {
this.loading = attachmentSettings.loading;
this.showStorageSettings = attachmentSettings.showStorageSettings;
this.showMigration = attachmentSettings.showMigration;
this.showMonitoring = attachmentSettings.showMonitoring;
// Set default sub-menu state
this.showStorageSettings.set(true);
this.showMigration.set(false);
this.showMonitoring.set(false);
// Load initial data
this.loadStorageConfiguration();
this.loadMigrationSettings();
this.loadMonitoringData();
},
// Template helpers for this component
loading() {
return this.loading.get();
},
showStorageSettings() {
return this.showStorageSettings.get();
},
showMigration() {
return this.showMigration.get();
},
showMonitoring() {
return this.showMonitoring.get();
},
events() {
return [
{
'click a.js-attachment-storage-settings': this.switchToStorageSettings,
'click a.js-attachment-migration': this.switchToMigration,
'click a.js-attachment-monitoring': this.switchToMonitoring,
}
];
},
switchToStorageSettings(event) {
this.switchMenu(event, 'storage-settings');
this.showStorageSettings.set(true);
this.showMigration.set(false);
this.showMonitoring.set(false);
},
switchToMigration(event) {
this.switchMenu(event, 'attachment-migration');
this.showStorageSettings.set(false);
this.showMigration.set(true);
this.showMonitoring.set(false);
},
switchToMonitoring(event) {
this.switchMenu(event, 'attachment-monitoring');
this.showStorageSettings.set(false);
this.showMigration.set(false);
this.showMonitoring.set(true);
},
switchMenu(event, targetId) {
const target = $(event.target);
if (!target.hasClass('active')) {
this.loading.set(true);
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
// Load data based on target
if (targetId === 'storage-settings') {
this.loadStorageConfiguration();
} else if (targetId === 'attachment-migration') {
this.loadMigrationSettings();
} else if (targetId === 'attachment-monitoring') {
this.loadMonitoringData();
}
this.loading.set(false);
}
},
loadStorageConfiguration() {
Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
if (!error && result) {
attachmentSettings.filesystemPath.set(result.filesystemPath || '');
attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
attachmentSettings.avatarsPath.set(result.avatarsPath || '');
attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
attachmentSettings.s3Enabled.set(result.s3Enabled || false);
attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
attachmentSettings.s3Bucket.set(result.s3Bucket || '');
attachmentSettings.s3Region.set(result.s3Region || '');
attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
attachmentSettings.s3Port.set(result.s3Port || 443);
}
});
},
loadMigrationSettings() {
Meteor.call('getAttachmentMigrationSettings', (error, result) => {
if (!error && result) {
attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
attachmentSettings.migrationStatus.set(result.status || 'idle');
attachmentSettings.migrationProgress.set(result.progress || 0);
}
});
},
loadMonitoringData() {
Meteor.call('getAttachmentMonitoringData', (error, result) => {
if (!error && result) {
attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
attachmentSettings.totalSize.set(result.totalSize || 0);
attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
attachmentSettings.s3Size.set(result.s3Size || 0);
}
});
}
}).register('attachmentSettings');
// Storage settings component
BlazeComponent.extendComponent({
onCreated() {
this.filesystemPath = attachmentSettings.filesystemPath;
this.attachmentsPath = attachmentSettings.attachmentsPath;
this.avatarsPath = attachmentSettings.avatarsPath;
this.gridfsEnabled = attachmentSettings.gridfsEnabled;
this.s3Enabled = attachmentSettings.s3Enabled;
this.s3Endpoint = attachmentSettings.s3Endpoint;
this.s3Bucket = attachmentSettings.s3Bucket;
this.s3Region = attachmentSettings.s3Region;
this.s3SslEnabled = attachmentSettings.s3SslEnabled;
this.s3Port = attachmentSettings.s3Port;
},
events() {
return [
{
'click button.js-test-s3-connection': this.testS3Connection,
'click button.js-save-s3-settings': this.saveS3Settings,
'change input#s3-secret-key': this.updateS3SecretKey
}
];
},
testS3Connection() {
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-connection-success'));
}
});
},
saveS3Settings() {
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-settings-saved'));
$('#s3-secret-key').val(''); // Clear the password field
}
});
},
updateS3SecretKey(event) {
// This method can be used to validate the secret key format
const secretKey = event.target.value;
// Add validation logic here if needed
}
}).register('storageSettings');
// Migration component
BlazeComponent.extendComponent({
onCreated() {
this.migrationBatchSize = attachmentSettings.migrationBatchSize;
this.migrationDelayMs = attachmentSettings.migrationDelayMs;
this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
this.migrationProgress = attachmentSettings.migrationProgress;
this.migrationStatus = attachmentSettings.migrationStatus;
this.migrationLog = attachmentSettings.migrationLog;
this.isMigrationRunning = attachmentSettings.isMigrationRunning;
this.isMigrationPaused = attachmentSettings.isMigrationPaused;
// Subscribe to migration updates
this.subscription = Meteor.subscribe('attachmentMigrationStatus');
// Set up reactive updates
this.autorun(() => {
const status = attachmentSettings.migrationStatus.get();
if (status === 'running') {
this.isMigrationRunning.set(true);
} else {
this.isMigrationRunning.set(false);
}
});
},
onDestroyed() {
if (this.subscription) {
this.subscription.stop();
}
},
events() {
return [
{
'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
'click button.js-pause-migration': this.pauseMigration,
'click button.js-resume-migration': this.resumeMigration,
'click button.js-stop-migration': this.stopMigration,
'change input#migration-batch-size': this.updateBatchSize,
'change input#migration-delay-ms': this.updateDelayMs,
'change input#migration-cpu-threshold': this.updateCpuThreshold
}
];
},
startMigration(targetStorage) {
const batchSize = parseInt($('#migration-batch-size').val()) || 10;
const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
Meteor.call('startAttachmentMigration', {
targetStorage,
batchSize,
delayMs,
cpuThreshold
}, (error, result) => {
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
}
});
},
pauseMigration() {
Meteor.call('pauseAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-paused'));
}
});
},
resumeMigration() {
Meteor.call('resumeAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-resumed'));
}
});
},
stopMigration() {
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
Meteor.call('stopAttachmentMigration', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
} else {
this.addToLog(TAPi18n.__('migration-stopped'));
}
});
}
},
updateBatchSize(event) {
const value = parseInt(event.target.value);
if (value >= 1 && value <= 100) {
attachmentSettings.migrationBatchSize.set(value);
}
},
updateDelayMs(event) {
const value = parseInt(event.target.value);
if (value >= 100 && value <= 10000) {
attachmentSettings.migrationDelayMs.set(value);
}
},
updateCpuThreshold(event) {
const value = parseInt(event.target.value);
if (value >= 10 && value <= 90) {
attachmentSettings.migrationCpuThreshold.set(value);
}
},
addToLog(message) {
const timestamp = new Date().toISOString();
const currentLog = attachmentSettings.migrationLog.get();
const newLog = `[${timestamp}] ${message}\n${currentLog}`;
attachmentSettings.migrationLog.set(newLog);
}
}).register('attachmentMigration');
// Monitoring component
BlazeComponent.extendComponent({
onCreated() {
this.totalAttachments = attachmentSettings.totalAttachments;
this.filesystemAttachments = attachmentSettings.filesystemAttachments;
this.gridfsAttachments = attachmentSettings.gridfsAttachments;
this.s3Attachments = attachmentSettings.s3Attachments;
this.totalSize = attachmentSettings.totalSize;
this.filesystemSize = attachmentSettings.filesystemSize;
this.gridfsSize = attachmentSettings.gridfsSize;
this.s3Size = attachmentSettings.s3Size;
// Subscribe to monitoring updates
this.subscription = Meteor.subscribe('attachmentMonitoringData');
// Set up chart
this.autorun(() => {
this.updateChart();
});
},
onDestroyed() {
if (this.subscription) {
this.subscription.stop();
}
},
events() {
return [
{
'click button.js-refresh-monitoring': this.refreshMonitoring,
'click button.js-export-monitoring': this.exportMonitoring
}
];
},
refreshMonitoring() {
Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
if (error) {
alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
}
});
},
exportMonitoring() {
Meteor.call('exportAttachmentMonitoringData', (error, result) => {
if (error) {
alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
} else {
// Download the exported data
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'wekan-attachment-monitoring.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
},
updateChart() {
const ctx = document.getElementById('storage-distribution-chart');
if (!ctx) return;
const filesystemCount = this.filesystemAttachments.get();
const gridfsCount = this.gridfsAttachments.get();
const s3Count = this.s3Attachments.get();
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: [
TAPi18n.__('filesystem-storage'),
TAPi18n.__('gridfs-storage'),
TAPi18n.__('s3-storage')
],
datasets: [{
data: [filesystemCount, gridfsCount, s3Count],
backgroundColor: [
'#28a745',
'#007bff',
'#ffc107'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
}).register('attachmentMonitoring');
// Template helpers for attachmentSettings
Template.attachmentSettings.helpers({
loading() {
const instance = Template.instance();
return instance.loading && instance.loading.get();
},
showStorageSettings() {
const instance = Template.instance();
return instance.showStorageSettings && instance.showStorageSettings.get();
},
showMigration() {
const instance = Template.instance();
return instance.showMigration && instance.showMigration.get();
},
showMonitoring() {
const instance = Template.instance();
return instance.showMonitoring && instance.showMonitoring.get();
},
});
// Export the attachment settings for use in other components
export { attachmentSettings };

View file

@ -1,35 +1,47 @@
template(name="cronSettings")
.cron-settings-content
.settings-tabs
ul.tab-nav
li(class="{{#if showMigrations}}active{{/if}}")
a.js-cron-migrations(data-id="cron-migrations")
i.fa.fa-database
| {{_ 'cron-migrations'}}
li(class="{{#if showBoardOperations}}active{{/if}}")
a.js-cron-board-operations(data-id="cron-board-operations")
i.fa.fa-tasks
| {{_ 'board-operations'}}
li(class="{{#if showJobs}}active{{/if}}")
a.js-cron-jobs(data-id="cron-jobs")
i.fa.fa-clock-o
| {{_ 'cron-jobs'}}
li(class="{{#if showAddJob}}active{{/if}}")
a.js-cron-add(data-id="cron-add")
i.fa.fa-plus
| {{_ 'add-cron-job'}}
ul#cron-setting.setting-detail
li
h3 {{_ 'cron-migrations'}}
.form-group
label {{_ 'migration-status'}}
.status-indicator
span.status-label {{_ 'status'}}:
span.status-value {{migrationStatus}}
.progress-section
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.progress-text
| {{migrationProgress}}% {{_ 'complete'}}
.form-group
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
.tab-content
if loading
+spinner
else if showMigrations
+cronMigrations
else if showBoardOperations
+cronBoardOperations
else if showJobs
+cronJobs
else if showAddJob
+cronAddJob
li
h3 {{_ 'board-operations'}}
.form-group
label {{_ 'scheduled-board-operations'}}
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
li
h3 {{_ 'cron-jobs'}}
.form-group
label {{_ 'active-cron-jobs'}}
each cronJobs
.job-item
.job-info
.job-name {{name}}
.job-schedule {{schedule}}
.job-description {{description}}
.job-actions
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
.add-job-section
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
template(name="cronMigrations")
.cron-migrations
@ -51,7 +63,7 @@ template(name="cronMigrations")
.migration-progress
.progress-overview
.progress-bar
.progress-fill(style="width: {{migrationProgress}}%")
.progress-fill(style="width: {{migrationProgress}}%")
.progress-text {{migrationProgress}}%
.progress-label {{_ 'overall-progress'}}

View file

@ -1,552 +0,0 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from '/imports/i18n';
// Reactive variables for cron settings
const migrationProgress = new ReactiveVar(0);
const migrationStatus = new ReactiveVar('');
const migrationCurrentStep = new ReactiveVar('');
const migrationSteps = new ReactiveVar([]);
const isMigrating = new ReactiveVar(false);
const cronJobs = new ReactiveVar([]);
Template.cronSettings.onCreated(function() {
this.loading = new ReactiveVar(true);
this.showMigrations = new ReactiveVar(true);
this.showBoardOperations = new ReactiveVar(false);
this.showJobs = new ReactiveVar(false);
this.showAddJob = new ReactiveVar(false);
// Board operations pagination
this.currentPage = new ReactiveVar(1);
this.pageSize = new ReactiveVar(20);
this.searchTerm = new ReactiveVar('');
this.boardOperations = new ReactiveVar([]);
this.operationStats = new ReactiveVar({});
this.pagination = new ReactiveVar({});
this.queueStats = new ReactiveVar({});
this.systemResources = new ReactiveVar({});
this.boardMigrationStats = new ReactiveVar({});
// Load initial data
loadCronData(this);
});
Template.cronSettings.helpers({
loading() {
const instance = Template.instance();
return instance && instance.loading ? instance.loading.get() : true;
},
showMigrations() {
const instance = Template.instance();
return instance && instance.showMigrations ? instance.showMigrations.get() : true;
},
showBoardOperations() {
const instance = Template.instance();
return instance && instance.showBoardOperations ? instance.showBoardOperations.get() : false;
},
showJobs() {
const instance = Template.instance();
return instance && instance.showJobs ? instance.showJobs.get() : false;
},
showAddJob() {
const instance = Template.instance();
return instance && instance.showAddJob ? instance.showAddJob.get() : false;
},
migrationProgress() {
return migrationProgress.get();
},
migrationStatus() {
return migrationStatus.get();
},
migrationCurrentStep() {
return migrationCurrentStep.get();
},
migrationSteps() {
const steps = migrationSteps.get();
const currentStep = migrationCurrentStep.get();
return steps.map(step => ({
...step,
isCurrentStep: step.name === currentStep
}));
},
cronJobs() {
return cronJobs.get();
},
formatDate(date) {
if (!date) return '-';
return new Date(date).toLocaleString();
},
boardOperations() {
const instance = Template.instance();
return instance && instance.boardOperations ? instance.boardOperations.get() : [];
},
operationStats() {
const instance = Template.instance();
return instance && instance.operationStats ? instance.operationStats.get() : {};
},
pagination() {
const instance = Template.instance();
return instance && instance.pagination ? instance.pagination.get() : {};
},
queueStats() {
const instance = Template.instance();
return instance && instance.queueStats ? instance.queueStats.get() : {};
},
systemResources() {
const instance = Template.instance();
return instance && instance.systemResources ? instance.systemResources.get() : {};
},
boardMigrationStats() {
const instance = Template.instance();
return instance && instance.boardMigrationStats ? instance.boardMigrationStats.get() : {};
},
formatDateTime(date) {
if (!date) return '-';
return new Date(date).toLocaleString();
},
formatDuration(startTime, endTime) {
if (!startTime) return '-';
const start = new Date(startTime);
const end = endTime ? new Date(endTime) : new Date();
const diffMs = end - start;
const diffMins = Math.floor(diffMs / 60000);
const diffSecs = Math.floor((diffMs % 60000) / 1000);
if (diffMins > 0) {
return `${diffMins}m ${diffSecs}s`;
} else {
return `${diffSecs}s`;
}
}
});
Template.cronSettings.switchMenu = function(event, targetID) {
const instance = Template.instance();
// Reset all tabs
instance.showMigrations.set(false);
instance.showBoardOperations.set(false);
instance.showJobs.set(false);
instance.showAddJob.set(false);
// Set the selected tab
if (targetID === 'cron-migrations') {
instance.showMigrations.set(true);
} else if (targetID === 'cron-board-operations') {
instance.showBoardOperations.set(true);
} else if (targetID === 'cron-jobs') {
instance.showJobs.set(true);
} else if (targetID === 'cron-add') {
instance.showAddJob.set(true);
}
};
Template.cronSettings.events({
'click .js-cron-migrations'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(true);
instance.showJobs.set(false);
instance.showAddJob.set(false);
},
'click .js-cron-board-operations'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(false);
instance.showBoardOperations.set(true);
instance.showJobs.set(false);
instance.showAddJob.set(false);
loadBoardOperations(instance);
},
'click .js-cron-jobs'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(false);
instance.showBoardOperations.set(false);
instance.showJobs.set(true);
instance.showAddJob.set(false);
loadCronJobs(instance);
},
'click .js-cron-add'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showMigrations.set(false);
instance.showJobs.set(false);
instance.showAddJob.set(true);
},
'click .js-start-all-migrations'(event) {
event.preventDefault();
Meteor.call('cron.startAllMigrations', (error, result) => {
if (error) {
console.error('Failed to start migrations:', error);
alert('Failed to start migrations: ' + error.message);
} else {
// Migrations started successfully
pollMigrationProgress(Template.instance());
}
});
},
'click .js-pause-all-migrations'(event) {
event.preventDefault();
// Pause all migration cron jobs
const jobs = cronJobs.get();
jobs.forEach(job => {
if (job.name.startsWith('migration_')) {
Meteor.call('cron.pauseJob', job.name);
}
});
},
'click .js-stop-all-migrations'(event) {
event.preventDefault();
// Stop all migration cron jobs
const jobs = cronJobs.get();
jobs.forEach(job => {
if (job.name.startsWith('migration_')) {
Meteor.call('cron.stopJob', job.name);
}
});
},
'click .js-refresh-jobs'(event) {
event.preventDefault();
loadCronJobs(Template.instance());
},
'click .js-start-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
Meteor.call('cron.startJob', jobName, (error, result) => {
if (error) {
console.error('Failed to start job:', error);
alert('Failed to start job: ' + error.message);
} else {
console.log('Job started successfully');
loadCronJobs(Template.instance());
}
});
},
'click .js-pause-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
Meteor.call('cron.pauseJob', jobName, (error, result) => {
if (error) {
console.error('Failed to pause job:', error);
alert('Failed to pause job: ' + error.message);
} else {
console.log('Job paused successfully');
loadCronJobs(Template.instance());
}
});
},
'click .js-stop-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
Meteor.call('cron.stopJob', jobName, (error, result) => {
if (error) {
console.error('Failed to stop job:', error);
alert('Failed to stop job: ' + error.message);
} else {
console.log('Job stopped successfully');
loadCronJobs(Template.instance());
}
});
},
'click .js-remove-job'(event) {
event.preventDefault();
const jobName = $(event.currentTarget).data('job');
if (confirm('Are you sure you want to remove this job?')) {
Meteor.call('cron.removeJob', jobName, (error, result) => {
if (error) {
console.error('Failed to remove job:', error);
alert('Failed to remove job: ' + error.message);
} else {
console.log('Job removed successfully');
loadCronJobs(Template.instance());
}
});
}
},
'submit .js-add-cron-job-form'(event) {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const jobData = {
name: formData.get('name'),
description: formData.get('description'),
schedule: formData.get('schedule'),
weight: parseInt(formData.get('weight'))
};
Meteor.call('cron.addJob', jobData, (error, result) => {
if (error) {
console.error('Failed to add job:', error);
alert('Failed to add job: ' + error.message);
} else {
console.log('Job added successfully');
form.reset();
Template.instance().showJobs.set(true);
Template.instance().showAddJob.set(false);
loadCronJobs(Template.instance());
}
});
},
'click .js-cancel-add-job'(event) {
event.preventDefault();
const instance = Template.instance();
instance.showJobs.set(true);
instance.showAddJob.set(false);
},
'click .js-refresh-board-operations'(event) {
event.preventDefault();
loadBoardOperations(Template.instance());
},
'click .js-start-test-operation'(event) {
event.preventDefault();
const testBoardId = 'test-board-' + Date.now();
const operationData = {
sourceBoardId: 'source-board',
targetBoardId: 'target-board',
copyOptions: { includeCards: true, includeAttachments: true }
};
Meteor.call('cron.startBoardOperation', testBoardId, 'copy_board', operationData, (error, result) => {
if (error) {
console.error('Failed to start test operation:', error);
alert('Failed to start test operation: ' + error.message);
} else {
console.log('Test operation started:', result);
loadBoardOperations(Template.instance());
}
});
},
'input .js-search-board-operations'(event) {
const searchTerm = $(event.currentTarget).val();
const instance = Template.instance();
instance.searchTerm.set(searchTerm);
instance.currentPage.set(1);
loadBoardOperations(instance);
},
'click .js-prev-page'(event) {
event.preventDefault();
const instance = Template.instance();
const currentPage = instance.currentPage.get();
if (currentPage > 1) {
instance.currentPage.set(currentPage - 1);
loadBoardOperations(instance);
}
},
'click .js-next-page'(event) {
event.preventDefault();
const instance = Template.instance();
const currentPage = instance.currentPage.get();
const pagination = instance.pagination.get();
if (currentPage < pagination.totalPages) {
instance.currentPage.set(currentPage + 1);
loadBoardOperations(instance);
}
},
'click .js-pause-operation'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
// Implementation for pausing operation
console.log('Pause operation:', operationId);
},
'click .js-resume-operation'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
// Implementation for resuming operation
console.log('Resume operation:', operationId);
},
'click .js-stop-operation'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
if (confirm('Are you sure you want to stop this operation?')) {
// Implementation for stopping operation
console.log('Stop operation:', operationId);
}
},
'click .js-view-details'(event) {
event.preventDefault();
const operationId = $(event.currentTarget).data('operation');
// Implementation for viewing operation details
console.log('View details for operation:', operationId);
},
'click .js-force-board-scan'(event) {
event.preventDefault();
Meteor.call('cron.forceBoardMigrationScan', (error, result) => {
if (error) {
console.error('Failed to force board scan:', error);
alert('Failed to force board scan: ' + error.message);
} else {
// Board scan started successfully
// Refresh the data
loadBoardOperations(Template.instance());
}
});
}
});
// Helper functions for cron settings
function loadCronData(instance) {
instance.loading.set(true);
// Load migration progress
Meteor.call('cron.getMigrationProgress', (error, result) => {
if (result) {
migrationProgress.set(result.progress);
migrationStatus.set(result.status);
migrationCurrentStep.set(result.currentStep);
migrationSteps.set(result.steps);
isMigrating.set(result.isMigrating);
}
});
// Load cron jobs
loadCronJobs(instance);
instance.loading.set(false);
}
function loadCronJobs(instance) {
Meteor.call('cron.getJobs', (error, result) => {
if (result) {
cronJobs.set(result);
}
});
}
function loadBoardOperations(instance) {
const page = instance.currentPage.get();
const limit = instance.pageSize.get();
const searchTerm = instance.searchTerm.get();
Meteor.call('cron.getAllBoardOperations', page, limit, searchTerm, (error, result) => {
if (result) {
instance.boardOperations.set(result.operations);
instance.pagination.set({
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
start: ((result.page - 1) * result.limit) + 1,
end: Math.min(result.page * result.limit, result.total),
hasPrev: result.page > 1,
hasNext: result.page < result.totalPages
});
}
});
// Load operation stats
Meteor.call('cron.getBoardOperationStats', (error, result) => {
if (result) {
instance.operationStats.set(result);
}
});
// Load queue stats
Meteor.call('cron.getQueueStats', (error, result) => {
if (result) {
instance.queueStats.set(result);
}
});
// Load system resources
Meteor.call('cron.getSystemResources', (error, result) => {
if (result) {
instance.systemResources.set(result);
}
});
// Load board migration stats
Meteor.call('cron.getBoardMigrationStats', (error, result) => {
if (result) {
instance.boardMigrationStats.set(result);
}
});
}
function pollMigrationProgress(instance) {
const pollInterval = setInterval(() => {
Meteor.call('cron.getMigrationProgress', (error, result) => {
if (result) {
migrationProgress.set(result.progress);
migrationStatus.set(result.status);
migrationCurrentStep.set(result.currentStep);
migrationSteps.set(result.steps);
isMigrating.set(result.isMigrating);
// Stop polling if migration is complete
if (!result.isMigrating && result.progress === 100) {
clearInterval(pollInterval);
}
}
});
}, 1000);
}
// Template helpers for cronSettings
Template.cronSettings.helpers({
loading() {
const instance = Template.instance();
return instance.loading && instance.loading.get();
},
showMigrations() {
const instance = Template.instance();
return instance.showMigrations && instance.showMigrations.get();
},
showBoardOperations() {
const instance = Template.instance();
return instance.showBoardOperations && instance.showBoardOperations.get();
},
showJobs() {
const instance = Template.instance();
return instance.showJobs && instance.showJobs.get();
},
showAddJob() {
const instance = Template.instance();
return instance.showAddJob && instance.showAddJob.get();
},
});

View file

@ -4,115 +4,224 @@ template(name="setting")
| {{_ 'error-notAuthorized'}}
else
.content-title.ext-box
if loading.get
+spinner
else if generalSetting.get
if isGeneralSetting
span
i.fa.fa-sign-in
| {{_ 'registration'}}
else if emailSetting.get
else if isEmailSetting
span
i.fa.fa-envelope
| {{_ 'email'}}
else if accountSetting.get
else if isAccountSetting
span
i.fa.fa-users
| {{_ 'accounts'}}
else if tableVisibilityModeSetting.get
else if isTableVisibilityModeSetting
span
i.fa.fa-eye
| {{_ 'tableVisibilityMode'}}
else if announcementSetting.get
else if isAnnouncementSetting
span
i.fa.fa-bullhorn
| {{_ 'admin-announcement'}}
else if accessibilitySetting.get
else if isAccessibilitySetting
span
i.fa.fa-universal-access
| {{_ 'accessibility'}}
else if layoutSetting.get
else if isLayoutSetting
span
i.fa.fa-object-group
| {{_ 'layout'}}
else if webhookSetting.get
else if isWebhookSetting
span
i.fa.fa-globe
| {{_ 'global-webhook'}}
else if attachmentSettings.get
else if isAttachmentSettings
span
i.fa.fa-paperclip
| {{_ 'attachments'}}
else if cronSettings.get
else if isCronSettings
span
i.fa.fa-clock-o
| {{_ 'cron'}}
.content-body
.side-menu
ul
li(class="{{#if generalSetting}}active{{/if}}")
li(class="{{#if isGeneralSetting}}active{{/if}}")
a.js-setting-menu(data-id="registration-setting")
i.fa.fa-sign-in
| {{_ 'registration'}}
unless isSandstorm
li(class="{{#if emailSetting}}active{{/if}}")
li(class="{{#if isEmailSetting}}active{{/if}}")
a.js-setting-menu(data-id="email-setting")
i.fa.fa-envelope
| {{_ 'email'}}
li(class="{{#if accountSetting}}active{{/if}}")
li(class="{{#if isAccountSetting}}active{{/if}}")
a.js-setting-menu(data-id="account-setting")
i.fa.fa-users
| {{_ 'accounts'}}
li(class="{{#if tableVisibilityModeSetting}}active{{/if}}")
li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
a.js-setting-menu(data-id="tableVisibilityMode-setting")
i.fa.fa-eye
| {{_ 'tableVisibilityMode'}}
li(class="{{#if announcementSetting}}active{{/if}}")
li(class="{{#if isAnnouncementSetting}}active{{/if}}")
a.js-setting-menu(data-id="announcement-setting")
i.fa.fa-bullhorn
| {{_ 'admin-announcement'}}
li(class="{{#if accessibilitySetting}}active{{/if}}")
li(class="{{#if isAccessibilitySetting}}active{{/if}}")
a.js-setting-menu(data-id="accessibility-setting")
i.fa.fa-universal-access
| {{_ 'accessibility'}}
li(class="{{#if layoutSetting}}active{{/if}}")
li(class="{{#if isLayoutSetting}}active{{/if}}")
a.js-setting-menu(data-id="layout-setting")
i.fa.fa-object-group
| {{_ 'layout'}}
li(class="{{#if webhookSetting}}active{{/if}}")
li(class="{{#if isWebhookSetting}}active{{/if}}")
a.js-setting-menu(data-id="webhook-setting")
i.fa.fa-globe
| {{_ 'global-webhook'}}
li(class="{{#if attachmentSettings}}active{{/if}}")
li(class="{{#if isAttachmentSettings}}active{{/if}}")
a.js-setting-menu(data-id="attachment-settings")
i.fa.fa-paperclip
| {{_ 'attachments'}}
li(class="{{#if cronSettings}}active{{/if}}")
li(class="{{#if isCronSettings}}active{{/if}}")
a.js-setting-menu(data-id="cron-settings")
i.fa.fa-clock-o
| {{_ 'cron'}}
.main-body
if loading.get
if isLoading
+spinner
else if attachmentSettings.get
+attachmentSettings
else if cronSettings.get
+cronSettings
else if generalSetting.get
else if isAttachmentSettings
ul#attachment-setting.setting-detail
li
h3 {{_ 'attachment-storage-configuration'}}
.form-group
label {{_ 'writable-path'}}
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
small.form-text.text-muted {{_ 'filesystem-path-description'}}
.form-group
label {{_ 'attachments-path'}}
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
small.form-text.text-muted {{_ 'attachments-path-description'}}
.form-group
label {{_ 'avatars-path'}}
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
small.form-text.text-muted {{_ 'avatars-path-description'}}
li
h3 {{_ 'mongodb-gridfs-storage'}}
.form-group
label {{_ 'gridfs-enabled'}}
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
li
h3 {{_ 's3-minio-storage'}}
.form-group
label {{_ 's3-enabled'}}
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
small.form-text.text-muted {{_ 's3-enabled-description'}}
.form-group
label {{_ 's3-endpoint'}}
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
small.form-text.text-muted {{_ 's3-endpoint-description'}}
.form-group
label {{_ 's3-bucket'}}
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
small.form-text.text-muted {{_ 's3-bucket-description'}}
.form-group
label {{_ 's3-region'}}
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
small.form-text.text-muted {{_ 's3-region-description'}}
.form-group
label {{_ 's3-access-key'}}
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
small.form-text.text-muted {{_ 's3-access-key-description'}}
.form-group
label {{_ 's3-secret-key'}}
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
small.form-text.text-muted {{_ 's3-secret-key-description'}}
.form-group
label {{_ 's3-ssl-enabled'}}
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
.form-group
label {{_ 's3-port'}}
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
small.form-text.text-muted {{_ 's3-port-description'}}
.form-group
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
else if isCronSettings
ul#cron-setting.setting-detail
li
h3 {{_ 'cron-migrations'}}
.form-group
label {{_ 'migration-status'}}
.status-indicator
span.status-label {{_ 'status'}}:
span.status-value {{migrationStatus}}
.progress-section
.progress
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
| {{migrationProgress}}%
.progress-text
| {{migrationProgress}}% {{_ 'complete'}}
.form-group
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
li
h3 {{_ 'board-operations'}}
.form-group
label {{_ 'scheduled-board-operations'}}
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
li
h3 {{_ 'cron-jobs'}}
.form-group
label {{_ 'active-cron-jobs'}}
each cronJobs
.job-item
.job-info
.job-name {{name}}
.job-schedule {{schedule}}
.job-description {{description}}
.job-actions
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
.add-job-section
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
else if isGeneralSetting
+general
else if emailSetting.get
else if isEmailSetting
unless isSandstorm
+email
else if accountSetting.get
else if isAccountSetting
+accountSettings
else if tableVisibilityModeSetting.get
else if isTableVisibilityModeSetting
+tableVisibilityModeSettings
else if announcementSetting.get
else if isAnnouncementSetting
+announcementSettings
else if accessibilitySetting.get
else if isAccessibilitySetting
+accessibilitySettings
else if layoutSetting.get
else if isLayoutSetting
+layoutSettings
else if webhookSetting.get
else if isWebhookSetting
+webhookSettings
template(name="webhookSettings")

View file

@ -34,11 +34,239 @@ BlazeComponent.extendComponent({
setError(error) {
this.error.set(error);
},
// Template helpers moved to BlazeComponent - using different names to avoid conflicts
isGeneralSetting() {
return this.generalSetting && this.generalSetting.get();
},
isEmailSetting() {
return this.emailSetting && this.emailSetting.get();
},
isAccountSetting() {
return this.accountSetting && this.accountSetting.get();
},
isTableVisibilityModeSetting() {
return this.tableVisibilityModeSetting && this.tableVisibilityModeSetting.get();
},
isAnnouncementSetting() {
return this.announcementSetting && this.announcementSetting.get();
},
isAccessibilitySetting() {
return this.accessibilitySetting && this.accessibilitySetting.get();
},
isLayoutSetting() {
return this.layoutSetting && this.layoutSetting.get();
},
isWebhookSetting() {
return this.webhookSetting && this.webhookSetting.get();
},
isAttachmentSettings() {
return this.attachmentSettings && this.attachmentSettings.get();
},
isCronSettings() {
return this.cronSettings && this.cronSettings.get();
},
isLoading() {
return this.loading && this.loading.get();
},
// Attachment settings helpers
filesystemPath() {
return process.env.WRITABLE_PATH || '/data';
},
attachmentsPath() {
const writablePath = process.env.WRITABLE_PATH || '/data';
return `${writablePath}/attachments`;
},
avatarsPath() {
const writablePath = process.env.WRITABLE_PATH || '/data';
return `${writablePath}/avatars`;
},
gridfsEnabled() {
return process.env.GRIDFS_ENABLED === 'true';
},
s3Enabled() {
return process.env.S3_ENABLED === 'true';
},
s3Endpoint() {
return process.env.S3_ENDPOINT || '';
},
s3Bucket() {
return process.env.S3_BUCKET || '';
},
s3Region() {
return process.env.S3_REGION || '';
},
s3SslEnabled() {
return process.env.S3_SSL_ENABLED === 'true';
},
s3Port() {
return process.env.S3_PORT || 443;
},
// Cron settings helpers
migrationStatus() {
return 'idle'; // Placeholder
},
migrationProgress() {
return 0; // Placeholder
},
cronJobs() {
return []; // Placeholder
},
setLoading(w) {
this.loading.set(w);
},
// Event handlers for attachment settings
'click button.js-test-s3-connection'(event) {
event.preventDefault();
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-connection-success'));
}
});
},
'click button.js-save-s3-settings'(event) {
event.preventDefault();
const secretKey = $('#s3-secret-key').val();
if (!secretKey) {
alert(TAPi18n.__('s3-secret-key-required'));
return;
}
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
if (error) {
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('s3-settings-saved'));
$('#s3-secret-key').val(''); // Clear the password field
}
});
},
// Event handlers for cron settings
'click button.js-start-all-migrations'(event) {
event.preventDefault();
Meteor.call('startAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-started'));
}
});
},
'click button.js-pause-all-migrations'(event) {
event.preventDefault();
Meteor.call('pauseAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-paused'));
}
});
},
'click button.js-stop-all-migrations'(event) {
event.preventDefault();
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
Meteor.call('stopAllMigrations', (error, result) => {
if (error) {
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('migration-stopped'));
}
});
}
},
'click button.js-schedule-board-cleanup'(event) {
event.preventDefault();
Meteor.call('scheduleBoardCleanup', (error, result) => {
if (error) {
alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-cleanup-scheduled'));
}
});
},
'click button.js-schedule-board-archive'(event) {
event.preventDefault();
Meteor.call('scheduleBoardArchive', (error, result) => {
if (error) {
alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-archive-scheduled'));
}
});
},
'click button.js-schedule-board-backup'(event) {
event.preventDefault();
Meteor.call('scheduleBoardBackup', (error, result) => {
if (error) {
alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('board-backup-scheduled'));
}
});
},
'click button.js-pause-job'(event) {
event.preventDefault();
const jobId = $(event.target).data('job-id');
Meteor.call('pauseCronJob', jobId, (error, result) => {
if (error) {
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('cron-job-paused'));
}
});
},
'click button.js-delete-job'(event) {
event.preventDefault();
const jobId = $(event.target).data('job-id');
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
Meteor.call('deleteCronJob', jobId, (error, result) => {
if (error) {
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
} else {
alert(TAPi18n.__('cron-job-deleted'));
}
});
}
},
'click button.js-add-cron-job'(event) {
event.preventDefault();
// Placeholder for adding a new cron job (e.g., open a modal)
alert(TAPi18n.__('add-cron-job-placeholder'));
},
checkField(selector) {
const value = $(selector).val();
if (!value || value.trim() === '') {
@ -104,37 +332,6 @@ BlazeComponent.extendComponent({
$('#display-authentication-method').toggleClass('is-checked');
},
switchAttachmentTab(event) {
event.preventDefault();
const target = $(event.target);
const targetID = target.data('id');
// Update active tab
$('.tab-nav li.active').removeClass('active');
target.parent().addClass('active');
// Call the attachment settings component method if available
if (window.attachmentSettings && window.attachmentSettings.switchMenu) {
window.attachmentSettings.switchMenu(event, targetID);
}
},
switchCronTab(event) {
event.preventDefault();
const target = $(event.target);
const targetID = target.data('id');
// Update active tab
$('.tab-nav li.active').removeClass('active');
target.parent().addClass('active');
// Call the cron settings template method if available
const cronTemplate = Template.instance();
if (cronTemplate && cronTemplate.switchMenu) {
cronTemplate.switchMenu(event, targetID);
}
},
initializeAttachmentSubMenu() {
// Set default sub-menu state for attachment settings
// This will be handled by the attachment settings component
@ -357,13 +554,6 @@ BlazeComponent.extendComponent({
'click button.js-save-layout': this.saveLayout,
'click a.js-toggle-display-authentication-method': this
.toggleDisplayAuthenticationMethod,
'click a.js-attachment-storage-settings': this.switchAttachmentTab,
'click a.js-attachment-migration': this.switchAttachmentTab,
'click a.js-attachment-monitoring': this.switchAttachmentTab,
'click a.js-cron-migrations': this.switchCronTab,
'click a.js-cron-board-operations': this.switchCronTab,
'click a.js-cron-jobs': this.switchCronTab,
'click a.js-cron-add': this.switchCronTab,
},
];
},
@ -609,50 +799,3 @@ Template.selectSpinnerName.helpers({
},
});
// Template helpers for the setting template
Template.setting.helpers({
generalSetting() {
const instance = Template.instance();
return instance.generalSetting && instance.generalSetting.get();
},
emailSetting() {
const instance = Template.instance();
return instance.emailSetting && instance.emailSetting.get();
},
accountSetting() {
const instance = Template.instance();
return instance.accountSetting && instance.accountSetting.get();
},
tableVisibilityModeSetting() {
const instance = Template.instance();
return instance.tableVisibilityModeSetting && instance.tableVisibilityModeSetting.get();
},
announcementSetting() {
const instance = Template.instance();
return instance.announcementSetting && instance.announcementSetting.get();
},
accessibilitySetting() {
const instance = Template.instance();
return instance.accessibilitySetting && instance.accessibilitySetting.get();
},
layoutSetting() {
const instance = Template.instance();
return instance.layoutSetting && instance.layoutSetting.get();
},
webhookSetting() {
const instance = Template.instance();
return instance.webhookSetting && instance.webhookSetting.get();
},
attachmentSettings() {
const instance = Template.instance();
return instance.attachmentSettings && instance.attachmentSettings.get();
},
cronSettings() {
const instance = Template.instance();
return instance.cronSettings && instance.cronSettings.get();
},
loading() {
const instance = Template.instance();
return instance.loading && instance.loading.get();
},
});

View file

@ -1,67 +0,0 @@
/* Settings Tabs Styles */
.settings-tabs {
margin-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
}
.tab-nav {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
}
.tab-nav li {
margin: 0;
padding: 0;
}
.tab-nav li a {
display: block;
padding: 12px 20px;
text-decoration: none;
color: #666;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
font-weight: 500;
}
.tab-nav li a:hover {
color: #333;
background-color: #f5f5f5;
}
.tab-nav li.active a {
color: #2196F3;
border-bottom-color: #2196F3;
background-color: #f8f9fa;
}
.tab-nav li a i {
margin-right: 8px;
width: 16px;
text-align: center;
}
.tab-content {
padding: 20px 0;
min-height: 400px;
}
/* Responsive design */
@media (max-width: 768px) {
.tab-nav {
flex-direction: column;
}
.tab-nav li a {
border-bottom: 1px solid #e0e0e0;
border-right: none;
}
.tab-nav li.active a {
border-bottom-color: #2196F3;
border-right: 3px solid #2196F3;
}
}