mirror of
https://github.com/wekan/wekan.git
synced 2026-02-20 23:14:07 +01:00
Merge branch 'main' into feature/reactive-cache-async-migration
This commit is contained in:
commit
5212f3beb3
328 changed files with 15124 additions and 3392 deletions
|
|
@ -18,44 +18,44 @@ AttachmentStorageSettings.attachSchema(
|
|||
defaultValue: STORAGE_NAME_FILESYSTEM,
|
||||
label: 'Default Storage Backend'
|
||||
},
|
||||
|
||||
|
||||
// Storage backend configuration
|
||||
storageConfig: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
label: 'Storage Configuration'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.filesystem': {
|
||||
type: Object,
|
||||
optional: true,
|
||||
label: 'Filesystem Configuration'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.filesystem.enabled': {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
label: 'Filesystem Storage Enabled'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.filesystem.path': {
|
||||
type: String,
|
||||
optional: true,
|
||||
label: 'Filesystem Storage Path'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.gridfs': {
|
||||
type: Object,
|
||||
optional: true,
|
||||
label: 'GridFS Configuration'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.gridfs.enabled': {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
label: 'GridFS Storage Enabled'
|
||||
},
|
||||
|
||||
|
||||
// DISABLED: S3 storage configuration removed due to Node.js compatibility
|
||||
/*
|
||||
'storageConfig.s3': {
|
||||
|
|
@ -63,81 +63,81 @@ AttachmentStorageSettings.attachSchema(
|
|||
optional: true,
|
||||
label: 'S3 Configuration'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.s3.enabled': {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
label: 'S3 Storage Enabled'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.s3.endpoint': {
|
||||
type: String,
|
||||
optional: true,
|
||||
label: 'S3 Endpoint'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.s3.bucket': {
|
||||
type: String,
|
||||
optional: true,
|
||||
label: 'S3 Bucket'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.s3.region': {
|
||||
type: String,
|
||||
optional: true,
|
||||
label: 'S3 Region'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.s3.sslEnabled': {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
label: 'S3 SSL Enabled'
|
||||
},
|
||||
|
||||
|
||||
'storageConfig.s3.port': {
|
||||
type: Number,
|
||||
defaultValue: 443,
|
||||
label: 'S3 Port'
|
||||
},
|
||||
*/
|
||||
|
||||
|
||||
// Upload settings
|
||||
uploadSettings: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
label: 'Upload Settings'
|
||||
},
|
||||
|
||||
|
||||
'uploadSettings.maxFileSize': {
|
||||
type: Number,
|
||||
optional: true,
|
||||
label: 'Maximum File Size (bytes)'
|
||||
},
|
||||
|
||||
|
||||
'uploadSettings.allowedMimeTypes': {
|
||||
type: Array,
|
||||
optional: true,
|
||||
label: 'Allowed MIME Types'
|
||||
},
|
||||
|
||||
|
||||
'uploadSettings.allowedMimeTypes.$': {
|
||||
type: String,
|
||||
label: 'MIME Type'
|
||||
},
|
||||
|
||||
|
||||
// Migration settings
|
||||
migrationSettings: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
label: 'Migration Settings'
|
||||
},
|
||||
|
||||
|
||||
'migrationSettings.autoMigrate': {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
label: 'Auto Migrate to Default Storage'
|
||||
},
|
||||
|
||||
|
||||
'migrationSettings.batchSize': {
|
||||
type: Number,
|
||||
defaultValue: 10,
|
||||
|
|
@ -145,7 +145,7 @@ AttachmentStorageSettings.attachSchema(
|
|||
max: 100,
|
||||
label: 'Migration Batch Size'
|
||||
},
|
||||
|
||||
|
||||
'migrationSettings.delayMs': {
|
||||
type: Number,
|
||||
defaultValue: 1000,
|
||||
|
|
@ -153,7 +153,7 @@ AttachmentStorageSettings.attachSchema(
|
|||
max: 10000,
|
||||
label: 'Migration Delay (ms)'
|
||||
},
|
||||
|
||||
|
||||
'migrationSettings.cpuThreshold': {
|
||||
type: Number,
|
||||
defaultValue: 70,
|
||||
|
|
@ -161,7 +161,7 @@ AttachmentStorageSettings.attachSchema(
|
|||
max: 90,
|
||||
label: 'CPU Threshold (%)'
|
||||
},
|
||||
|
||||
|
||||
// Metadata
|
||||
createdAt: {
|
||||
type: Date,
|
||||
|
|
@ -176,7 +176,7 @@ AttachmentStorageSettings.attachSchema(
|
|||
},
|
||||
label: 'Created At'
|
||||
},
|
||||
|
||||
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
autoValue() {
|
||||
|
|
@ -186,13 +186,13 @@ AttachmentStorageSettings.attachSchema(
|
|||
},
|
||||
label: 'Updated At'
|
||||
},
|
||||
|
||||
|
||||
createdBy: {
|
||||
type: String,
|
||||
optional: true,
|
||||
label: 'Created By'
|
||||
},
|
||||
|
||||
|
||||
updatedBy: {
|
||||
type: String,
|
||||
optional: true,
|
||||
|
|
@ -207,11 +207,11 @@ AttachmentStorageSettings.helpers({
|
|||
getDefaultStorage() {
|
||||
return this.defaultStorage || STORAGE_NAME_FILESYSTEM;
|
||||
},
|
||||
|
||||
|
||||
// Check if storage backend is enabled
|
||||
isStorageEnabled(storageName) {
|
||||
if (!this.storageConfig) return false;
|
||||
|
||||
|
||||
switch (storageName) {
|
||||
case STORAGE_NAME_FILESYSTEM:
|
||||
return this.storageConfig.filesystem?.enabled !== false;
|
||||
|
|
@ -224,11 +224,11 @@ AttachmentStorageSettings.helpers({
|
|||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Get storage configuration
|
||||
getStorageConfig(storageName) {
|
||||
if (!this.storageConfig) return null;
|
||||
|
||||
|
||||
switch (storageName) {
|
||||
case STORAGE_NAME_FILESYSTEM:
|
||||
return this.storageConfig.filesystem;
|
||||
|
|
@ -241,12 +241,12 @@ AttachmentStorageSettings.helpers({
|
|||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Get upload settings
|
||||
getUploadSettings() {
|
||||
return this.uploadSettings || {};
|
||||
},
|
||||
|
||||
|
||||
// Get migration settings
|
||||
getMigrationSettings() {
|
||||
return this.migrationSettings || {};
|
||||
|
|
@ -335,7 +335,7 @@ if (Meteor.isServer) {
|
|||
|
||||
return result;
|
||||
},
|
||||
|
||||
|
||||
'getDefaultAttachmentStorage'() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
|
|
|
|||
|
|
@ -943,7 +943,8 @@ Boards.helpers({
|
|||
async activeMembers(){
|
||||
// Depend on the users collection for reactivity when users are loaded
|
||||
const memberUserIds = _.pluck(this.members, 'userId');
|
||||
const dummy = Meteor.users.find({ _id: { $in: memberUserIds } }).count();
|
||||
// Use findOne with limit for reactivity trigger instead of count() which loads all users
|
||||
const dummy = Meteor.users.findOne({ _id: { $in: memberUserIds } }, { fields: { _id: 1 }, limit: 1 });
|
||||
const members = _.filter(this.members, m => m.isActive === true);
|
||||
// Group by userId to handle duplicates
|
||||
const grouped = _.groupBy(members, 'userId');
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import { ReactiveCache, ReactiveMiniMongoIndex } from '/imports/reactiveCache';
|
||||
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import {
|
||||
ALLOWED_COLORS,
|
||||
|
|
@ -573,6 +573,10 @@ Cards.helpers({
|
|||
},
|
||||
|
||||
async mapCustomFieldsToBoard(boardId) {
|
||||
// Guard against undefined/null customFields
|
||||
if (!this.customFields || !Array.isArray(this.customFields)) {
|
||||
return [];
|
||||
}
|
||||
// Map custom fields to new board
|
||||
const result = [];
|
||||
for (const cf of this.customFields) {
|
||||
|
|
@ -607,6 +611,15 @@ Cards.helpers({
|
|||
const oldId = this._id;
|
||||
const oldCard = await ReactiveCache.getCard(oldId);
|
||||
|
||||
// Work on a shallow copy to avoid mutating the source card in ReactiveCache
|
||||
const cardData = { ...this };
|
||||
delete cardData._id;
|
||||
|
||||
// Normalize customFields to ensure it's always an array
|
||||
if (!Array.isArray(cardData.customFields)) {
|
||||
cardData.customFields = [];
|
||||
}
|
||||
|
||||
// we must only copy the labels and custom fields if the target board
|
||||
// differs from the source board
|
||||
if (this.boardId !== boardId) {
|
||||
|
|
@ -629,9 +642,7 @@ Cards.helpers({
|
|||
}),
|
||||
'_id',
|
||||
);
|
||||
// now set the new label ids
|
||||
delete this.labelIds;
|
||||
this.labelIds = newCardLabels;
|
||||
cardData.labelIds = newCardLabels;
|
||||
|
||||
this.customFields = await this.mapCustomFieldsToBoard(newBoard._id);
|
||||
}
|
||||
|
|
@ -2105,6 +2116,11 @@ Cards.helpers({
|
|||
});
|
||||
|
||||
mutatedFields.customFields = await this.mapCustomFieldsToBoard(newBoard._id);
|
||||
|
||||
// Ensure customFields is always an array (guards against legacy {} data)
|
||||
if (!Array.isArray(mutatedFields.customFields)) {
|
||||
mutatedFields.customFields = [];
|
||||
}
|
||||
}
|
||||
|
||||
await Cards.updateAsync(this._id, { $set: mutatedFields });
|
||||
|
|
@ -4309,10 +4325,10 @@ Cards.helpers({
|
|||
hasMovedFromOriginalPosition() {
|
||||
const history = this.getOriginalPosition();
|
||||
if (!history) return false;
|
||||
|
||||
|
||||
const currentSwimlaneId = this.swimlaneId || null;
|
||||
const currentListId = this.listId || null;
|
||||
|
||||
|
||||
return history.originalPosition.sort !== this.sort ||
|
||||
history.originalSwimlaneId !== currentSwimlaneId ||
|
||||
history.originalListId !== currentListId;
|
||||
|
|
@ -4324,12 +4340,12 @@ Cards.helpers({
|
|||
getOriginalPositionDescription() {
|
||||
const history = this.getOriginalPosition();
|
||||
if (!history) return 'No original position data';
|
||||
|
||||
const swimlaneInfo = history.originalSwimlaneId ?
|
||||
` in swimlane ${history.originalSwimlaneId}` :
|
||||
|
||||
const swimlaneInfo = history.originalSwimlaneId ?
|
||||
` in swimlane ${history.originalSwimlaneId}` :
|
||||
' in default swimlane';
|
||||
const listInfo = history.originalListId ?
|
||||
` in list ${history.originalListId}` :
|
||||
const listInfo = history.originalListId ?
|
||||
` in list ${history.originalListId}` :
|
||||
'';
|
||||
return `Original position: ${history.originalPosition.sort || 0}${swimlaneInfo}${listInfo}`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ import { mongodbDriverManager } from './mongodbDriverManager';
|
|||
|
||||
/**
|
||||
* Meteor MongoDB Integration
|
||||
*
|
||||
*
|
||||
* This module integrates the MongoDB driver manager with Meteor's
|
||||
* built-in MongoDB connection system to provide automatic driver
|
||||
* selection and version detection.
|
||||
*
|
||||
*
|
||||
* Features:
|
||||
* - Hooks into Meteor's MongoDB connection process
|
||||
* - Automatic driver selection based on detected version
|
||||
|
|
@ -58,7 +58,7 @@ class MeteorMongoIntegration {
|
|||
*/
|
||||
overrideMeteorConnection() {
|
||||
const self = this;
|
||||
|
||||
|
||||
// Override Meteor.connect if it exists
|
||||
if (typeof Meteor.connect === 'function') {
|
||||
Meteor.connect = async function(url, options) {
|
||||
|
|
@ -110,16 +110,16 @@ class MeteorMongoIntegration {
|
|||
async createCustomConnection(url, options = {}) {
|
||||
try {
|
||||
console.log('Creating custom MongoDB connection...');
|
||||
|
||||
|
||||
// Use our connection manager
|
||||
const connection = await mongodbConnectionManager.createConnection(url, options);
|
||||
|
||||
|
||||
// Store the custom connection
|
||||
this.customConnection = connection;
|
||||
|
||||
|
||||
// Create a Meteor-compatible connection object
|
||||
const meteorConnection = this.createMeteorCompatibleConnection(connection);
|
||||
|
||||
|
||||
console.log('Custom MongoDB connection created successfully');
|
||||
return meteorConnection;
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ class MeteorMongoIntegration {
|
|||
// Basic connection properties
|
||||
_driver: connection,
|
||||
_name: 'custom-mongodb-connection',
|
||||
|
||||
|
||||
// Collection creation method
|
||||
createCollection: function(name, options = {}) {
|
||||
const db = connection.db();
|
||||
|
|
@ -242,7 +242,7 @@ class MeteorMongoIntegration {
|
|||
if (this.originalMongoConnect) {
|
||||
Meteor.connect = this.originalMongoConnect;
|
||||
}
|
||||
|
||||
|
||||
if (this.originalMongoCollection) {
|
||||
Mongo.Collection = this.originalMongoCollection;
|
||||
}
|
||||
|
|
@ -269,7 +269,7 @@ class MeteorMongoIntegration {
|
|||
|
||||
const db = this.customConnection.db();
|
||||
const result = await db.admin().ping();
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { mongodbDriverManager } from './mongodbDriverManager';
|
|||
|
||||
/**
|
||||
* MongoDB Connection Manager
|
||||
*
|
||||
*
|
||||
* This module handles MongoDB connections with automatic driver selection
|
||||
* based on detected MongoDB server version and wire protocol compatibility.
|
||||
*
|
||||
*
|
||||
* Features:
|
||||
* - Automatic driver selection based on MongoDB version
|
||||
* - Connection retry with different drivers on wire protocol errors
|
||||
|
|
@ -30,7 +30,7 @@ class MongoDBConnectionManager {
|
|||
*/
|
||||
async createConnection(connectionString, options = {}) {
|
||||
const connectionId = this.generateConnectionId(connectionString);
|
||||
|
||||
|
||||
// Check if we already have a working connection
|
||||
if (this.connections.has(connectionId)) {
|
||||
const existingConnection = this.connections.get(connectionId);
|
||||
|
|
@ -66,13 +66,13 @@ class MongoDBConnectionManager {
|
|||
for (let attempt = 0; attempt < this.retryAttempts; attempt++) {
|
||||
try {
|
||||
console.log(`Attempting MongoDB connection with driver: ${currentDriver} (attempt ${attempt + 1})`);
|
||||
|
||||
|
||||
const connection = await this.connectWithDriver(currentDriver, connectionString, options);
|
||||
|
||||
|
||||
// Record successful connection
|
||||
mongodbDriverManager.recordConnectionAttempt(
|
||||
currentDriver,
|
||||
mongodbDriverManager.detectedVersion || 'unknown',
|
||||
currentDriver,
|
||||
mongodbDriverManager.detectedVersion || 'unknown',
|
||||
true
|
||||
);
|
||||
|
||||
|
|
@ -113,9 +113,9 @@ class MongoDBConnectionManager {
|
|||
|
||||
// Record failed attempt
|
||||
mongodbDriverManager.recordConnectionAttempt(
|
||||
currentDriver,
|
||||
detectedVersion || 'unknown',
|
||||
false,
|
||||
currentDriver,
|
||||
detectedVersion || 'unknown',
|
||||
false,
|
||||
error
|
||||
);
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ class MongoDBConnectionManager {
|
|||
async closeAllConnections() {
|
||||
let closedCount = 0;
|
||||
const connectionIds = Array.from(this.connections.keys());
|
||||
|
||||
|
||||
for (const connectionId of connectionIds) {
|
||||
if (await this.closeConnection(connectionId)) {
|
||||
closedCount++;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { Meteor } from 'meteor/meteor';
|
|||
|
||||
/**
|
||||
* MongoDB Driver Manager
|
||||
*
|
||||
*
|
||||
* This module provides automatic MongoDB version detection and driver selection
|
||||
* to support MongoDB versions 3.0 through 8.0 with compatible Node.js drivers.
|
||||
*
|
||||
*
|
||||
* Features:
|
||||
* - Automatic MongoDB version detection from wire protocol errors
|
||||
* - Dynamic driver selection based on detected version
|
||||
|
|
@ -113,7 +113,7 @@ class MongoDBDriverManager {
|
|||
}
|
||||
|
||||
const errorMessage = error.message.toLowerCase();
|
||||
|
||||
|
||||
// Check specific version patterns
|
||||
for (const [version, patterns] of Object.entries(VERSION_ERROR_PATTERNS)) {
|
||||
for (const pattern of patterns) {
|
||||
|
|
|
|||
|
|
@ -61,10 +61,10 @@ export function cleanFileUrl(url, type) {
|
|||
|
||||
// Remove any domain, port, or protocol from the URL
|
||||
let cleanUrl = url;
|
||||
|
||||
|
||||
// Remove protocol and domain
|
||||
cleanUrl = cleanUrl.replace(/^https?:\/\/[^\/]+/, '');
|
||||
|
||||
|
||||
// Remove ROOT_URL pathname if present
|
||||
if (Meteor.isServer && process.env.ROOT_URL) {
|
||||
try {
|
||||
|
|
@ -79,7 +79,7 @@ export function cleanFileUrl(url, type) {
|
|||
|
||||
// Normalize path separators
|
||||
cleanUrl = cleanUrl.replace(/\/+/g, '/');
|
||||
|
||||
|
||||
// Ensure URL starts with /
|
||||
if (!cleanUrl.startsWith('/')) {
|
||||
cleanUrl = '/' + cleanUrl;
|
||||
|
|
@ -176,13 +176,13 @@ export function getAllPossibleUrls(fileId, type) {
|
|||
}
|
||||
|
||||
const urls = [];
|
||||
|
||||
|
||||
// Primary URL
|
||||
urls.push(generateUniversalFileUrl(fileId, type));
|
||||
|
||||
|
||||
// Fallback URL
|
||||
urls.push(generateFallbackUrl(fileId, type));
|
||||
|
||||
|
||||
// Legacy URLs for backward compatibility
|
||||
if (type === 'attachment') {
|
||||
urls.push(`/cfs/files/attachments/${fileId}`);
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ export function isValidBoolean(value) {
|
|||
*/
|
||||
export function getValidatedNumber(key, boardId, itemId, defaultValue, min, max) {
|
||||
if (typeof localStorage === 'undefined') return defaultValue;
|
||||
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (!stored) return defaultValue;
|
||||
|
||||
|
||||
const data = JSON.parse(stored);
|
||||
if (data[boardId] && typeof data[boardId][itemId] === 'number') {
|
||||
const value = data[boardId][itemId];
|
||||
|
|
@ -41,7 +41,7 @@ export function getValidatedNumber(key, boardId, itemId, defaultValue, min, max)
|
|||
} catch (e) {
|
||||
console.warn(`Error reading ${key} from localStorage:`, e);
|
||||
}
|
||||
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
|
@ -50,22 +50,22 @@ export function getValidatedNumber(key, boardId, itemId, defaultValue, min, max)
|
|||
*/
|
||||
export function setValidatedNumber(key, boardId, itemId, value, min, max) {
|
||||
if (typeof localStorage === 'undefined') return false;
|
||||
|
||||
|
||||
// Validate value
|
||||
if (typeof value !== 'number' || isNaN(value) || !isFinite(value) || value < min || value > max) {
|
||||
console.warn(`Invalid value for ${key}:`, value);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
const data = stored ? JSON.parse(stored) : {};
|
||||
|
||||
|
||||
if (!data[boardId]) {
|
||||
data[boardId] = {};
|
||||
}
|
||||
data[boardId][itemId] = value;
|
||||
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -79,11 +79,11 @@ export function setValidatedNumber(key, boardId, itemId, value, min, max) {
|
|||
*/
|
||||
export function getValidatedBoolean(key, boardId, itemId, defaultValue) {
|
||||
if (typeof localStorage === 'undefined') return defaultValue;
|
||||
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (!stored) return defaultValue;
|
||||
|
||||
|
||||
const data = JSON.parse(stored);
|
||||
if (data[boardId] && typeof data[boardId][itemId] === 'boolean') {
|
||||
return data[boardId][itemId];
|
||||
|
|
@ -91,7 +91,7 @@ export function getValidatedBoolean(key, boardId, itemId, defaultValue) {
|
|||
} catch (e) {
|
||||
console.warn(`Error reading ${key} from localStorage:`, e);
|
||||
}
|
||||
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
|
@ -100,22 +100,22 @@ export function getValidatedBoolean(key, boardId, itemId, defaultValue) {
|
|||
*/
|
||||
export function setValidatedBoolean(key, boardId, itemId, value) {
|
||||
if (typeof localStorage === 'undefined') return false;
|
||||
|
||||
|
||||
// Validate value
|
||||
if (typeof value !== 'boolean') {
|
||||
console.warn(`Invalid boolean value for ${key}:`, value);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
const data = stored ? JSON.parse(stored) : {};
|
||||
|
||||
|
||||
if (!data[boardId]) {
|
||||
data[boardId] = {};
|
||||
}
|
||||
data[boardId][itemId] = value;
|
||||
|
||||
|
||||
localStorage.setItem(key, JSON.stringify(data));
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -225,6 +225,8 @@ Lists.helpers({
|
|||
for (const card of cards) {
|
||||
await card.copy(boardId, swimlaneId, _id);
|
||||
}
|
||||
|
||||
return _id;
|
||||
},
|
||||
|
||||
async move(boardId, swimlaneId) {
|
||||
|
|
|
|||
|
|
@ -139,17 +139,33 @@ if (Meteor.isServer) {
|
|||
|
||||
LockoutSettings.helpers({
|
||||
getKnownConfig() {
|
||||
// Fetch all settings in one query instead of 3 separate queries
|
||||
const settings = LockoutSettings.find({
|
||||
_id: { $in: ['known-failuresBeforeLockout', 'known-lockoutPeriod', 'known-failureWindow'] }
|
||||
}, { fields: { _id: 1, value: 1 } }).fetch();
|
||||
|
||||
const settingsMap = {};
|
||||
settings.forEach(s => { settingsMap[s._id] = s.value; });
|
||||
|
||||
return {
|
||||
failuresBeforeLockout: LockoutSettings.findOne('known-failuresBeforeLockout')?.value || 3,
|
||||
lockoutPeriod: LockoutSettings.findOne('known-lockoutPeriod')?.value || 60,
|
||||
failureWindow: LockoutSettings.findOne('known-failureWindow')?.value || 15
|
||||
failuresBeforeLockout: settingsMap['known-failuresBeforeLockout'] || 3,
|
||||
lockoutPeriod: settingsMap['known-lockoutPeriod'] || 60,
|
||||
failureWindow: settingsMap['known-failureWindow'] || 15
|
||||
};
|
||||
},
|
||||
getUnknownConfig() {
|
||||
// Fetch all settings in one query instead of 3 separate queries
|
||||
const settings = LockoutSettings.find({
|
||||
_id: { $in: ['unknown-failuresBeforeLockout', 'unknown-lockoutPeriod', 'unknown-failureWindow'] }
|
||||
}, { fields: { _id: 1, value: 1 } }).fetch();
|
||||
|
||||
const settingsMap = {};
|
||||
settings.forEach(s => { settingsMap[s._id] = s.value; });
|
||||
|
||||
return {
|
||||
failuresBeforeLockout: LockoutSettings.findOne('unknown-failuresBeforeLockout')?.value || 3,
|
||||
lockoutPeriod: LockoutSettings.findOne('unknown-lockoutPeriod')?.value || 60,
|
||||
failureWindow: LockoutSettings.findOne('unknown-failureWindow')?.value || 15
|
||||
failuresBeforeLockout: settingsMap['unknown-failuresBeforeLockout'] || 3,
|
||||
lockoutPeriod: settingsMap['unknown-lockoutPeriod'] || 60,
|
||||
failureWindow: settingsMap['unknown-failureWindow'] || 15
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,6 +118,34 @@ Settings.attachSchema(
|
|||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
customHeadEnabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
customHeadMetaTags: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
customHeadLinkTags: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
customManifestEnabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
customManifestContent: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
customAssetLinksEnabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
customAssetLinksContent: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
accessibilityPageEnabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ Swimlanes.allow({
|
|||
});
|
||||
|
||||
Swimlanes.helpers({
|
||||
async copy(boardId) {
|
||||
async copy(boardId) {
|
||||
const oldId = this._id;
|
||||
const oldBoardId = this.boardId;
|
||||
|
|
@ -170,6 +171,8 @@ Swimlanes.helpers({
|
|||
list.boardId = boardId;
|
||||
await list.copy(boardId, _id);
|
||||
}
|
||||
|
||||
return _id;
|
||||
},
|
||||
|
||||
async move(toBoardId) {
|
||||
|
|
@ -691,7 +694,7 @@ Swimlanes.helpers({
|
|||
hasMovedFromOriginalPosition() {
|
||||
const history = this.getOriginalPosition();
|
||||
if (!history) return false;
|
||||
|
||||
|
||||
return history.originalPosition.sort !== this.sort;
|
||||
},
|
||||
|
||||
|
|
@ -701,7 +704,7 @@ Swimlanes.helpers({
|
|||
getOriginalPositionDescription() {
|
||||
const history = this.getOriginalPosition();
|
||||
if (!history) return 'No original position data';
|
||||
|
||||
|
||||
return `Original position: ${history.originalPosition.sort || 0}`;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -155,9 +155,9 @@ UserPositionHistory.helpers({
|
|||
getDescription() {
|
||||
const entityName = this.entityType;
|
||||
const action = this.actionType;
|
||||
|
||||
|
||||
let desc = `${action} ${entityName}`;
|
||||
|
||||
|
||||
if (this.actionType === 'move') {
|
||||
if (this.previousListId && this.newListId && this.previousListId !== this.newListId) {
|
||||
desc += ' to different list';
|
||||
|
|
@ -167,7 +167,7 @@ UserPositionHistory.helpers({
|
|||
desc += ' position';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return desc;
|
||||
},
|
||||
|
||||
|
|
@ -348,20 +348,20 @@ if (Meteor.isServer) {
|
|||
* Cleanup old history entries (keep last 1000 per user per board)
|
||||
*/
|
||||
UserPositionHistory.cleanup = function() {
|
||||
const users = Meteor.users.find({}).fetch();
|
||||
|
||||
const users = Meteor.users.find({}, { fields: { _id: 1 } }).fetch();
|
||||
|
||||
users.forEach(user => {
|
||||
const boards = Boards.find({ 'members.userId': user._id }).fetch();
|
||||
|
||||
const boards = Boards.find({ 'members.userId': user._id }, { fields: { _id: 1 } }).fetch();
|
||||
|
||||
boards.forEach(board => {
|
||||
const history = UserPositionHistory.find(
|
||||
{ userId: user._id, boardId: board._id, isCheckpoint: { $ne: true } },
|
||||
{ sort: { createdAt: -1 }, limit: 1000 }
|
||||
).fetch();
|
||||
|
||||
|
||||
if (history.length >= 1000) {
|
||||
const oldestToKeep = history[999].createdAt;
|
||||
|
||||
|
||||
// Remove entries older than the 1000th entry (except checkpoints)
|
||||
UserPositionHistory.remove({
|
||||
userId: user._id,
|
||||
|
|
@ -391,11 +391,11 @@ Meteor.methods({
|
|||
'userPositionHistory.createCheckpoint'(boardId, checkpointName) {
|
||||
check(boardId, String);
|
||||
check(checkpointName, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
// Create a checkpoint entry
|
||||
return UserPositionHistory.insert({
|
||||
userId: this.userId,
|
||||
|
|
@ -429,11 +429,11 @@ Meteor.methods({
|
|||
'userPositionHistory.getRecent'(boardId, limit = 50) {
|
||||
check(boardId, String);
|
||||
check(limit, Number);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return UserPositionHistory.find(
|
||||
{ userId: this.userId, boardId },
|
||||
{ sort: { createdAt: -1 }, limit: Math.min(limit, 100) }
|
||||
|
|
@ -442,11 +442,11 @@ Meteor.methods({
|
|||
|
||||
'userPositionHistory.getCheckpoints'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return UserPositionHistory.find(
|
||||
{ userId: this.userId, boardId, isCheckpoint: true },
|
||||
{ sort: { createdAt: -1 } }
|
||||
|
|
@ -491,7 +491,7 @@ Meteor.methods({
|
|||
} catch (e) {
|
||||
console.warn('Failed to undo change:', change._id, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { undoneCount, totalChanges: changesToUndo.length };
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1103,7 +1103,7 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.getSwimlaneHeight(boardId, swimlaneId);
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, get from localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('wekan-swimlane-heights');
|
||||
|
|
@ -1116,7 +1116,7 @@ Users.helpers({
|
|||
} catch (e) {
|
||||
console.warn('Error reading swimlane heights from localStorage:', e);
|
||||
}
|
||||
|
||||
|
||||
return -1;
|
||||
},
|
||||
|
||||
|
|
@ -1125,17 +1125,17 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.setSwimlaneHeight(boardId, swimlaneId, height);
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, save to localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('wekan-swimlane-heights');
|
||||
let heights = stored ? JSON.parse(stored) : {};
|
||||
|
||||
|
||||
if (!heights[boardId]) {
|
||||
heights[boardId] = {};
|
||||
}
|
||||
heights[boardId][swimlaneId] = height;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-swimlane-heights', JSON.stringify(heights));
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -1322,7 +1322,7 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.getListWidth(boardId, listId);
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, get from validated localStorage
|
||||
if (typeof localStorage !== 'undefined' && typeof getValidatedLocalStorageData === 'function') {
|
||||
try {
|
||||
|
|
@ -1330,7 +1330,7 @@ Users.helpers({
|
|||
if (widths[boardId] && widths[boardId][listId]) {
|
||||
const width = widths[boardId][listId];
|
||||
// Validate it's a valid number
|
||||
if (validators.isValidNumber(width, 100, 1000)) {
|
||||
if (validators.isValidNumber(width, 270, 1000)) {
|
||||
return width;
|
||||
}
|
||||
}
|
||||
|
|
@ -1338,7 +1338,7 @@ Users.helpers({
|
|||
console.warn('Error reading list widths from localStorage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return 270; // Return default width
|
||||
},
|
||||
|
||||
|
|
@ -1347,23 +1347,23 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.setListWidth(boardId, listId, width);
|
||||
}
|
||||
|
||||
|
||||
// Validate width before storing
|
||||
if (!validators.isValidNumber(width, 100, 1000)) {
|
||||
if (!validators.isValidNumber(width, 270, 1000)) {
|
||||
console.warn('Invalid list width:', width);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, save to validated localStorage
|
||||
if (typeof localStorage !== 'undefined' && typeof setValidatedLocalStorageData === 'function') {
|
||||
try {
|
||||
const widths = getValidatedLocalStorageData('wekan-list-widths', validators.listWidths);
|
||||
|
||||
|
||||
if (!widths[boardId]) {
|
||||
widths[boardId] = {};
|
||||
}
|
||||
widths[boardId][listId] = width;
|
||||
|
||||
|
||||
return setValidatedLocalStorageData('wekan-list-widths', widths, validators.listWidths);
|
||||
} catch (e) {
|
||||
console.warn('Error saving list width to localStorage:', e);
|
||||
|
|
@ -1378,7 +1378,7 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.getListConstraint(boardId, listId);
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, get from localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('wekan-list-constraints');
|
||||
|
|
@ -1391,7 +1391,7 @@ Users.helpers({
|
|||
} catch (e) {
|
||||
console.warn('Error reading list constraints from localStorage:', e);
|
||||
}
|
||||
|
||||
|
||||
return 550; // Return default constraint instead of -1
|
||||
},
|
||||
|
||||
|
|
@ -1400,17 +1400,17 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.setListConstraint(boardId, listId, constraint);
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, save to localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('wekan-list-constraints');
|
||||
let constraints = stored ? JSON.parse(stored) : {};
|
||||
|
||||
|
||||
if (!constraints[boardId]) {
|
||||
constraints[boardId] = {};
|
||||
}
|
||||
constraints[boardId][listId] = constraint;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -1424,7 +1424,7 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.getSwimlaneHeight(boardId, swimlaneId);
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, get from localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('wekan-swimlane-heights');
|
||||
|
|
@ -1437,7 +1437,7 @@ Users.helpers({
|
|||
} catch (e) {
|
||||
console.warn('Error reading swimlane heights from localStorage:', e);
|
||||
}
|
||||
|
||||
|
||||
return -1; // Return -1 if not found
|
||||
},
|
||||
|
||||
|
|
@ -1446,17 +1446,17 @@ Users.helpers({
|
|||
if (this._id) {
|
||||
return this.setSwimlaneHeight(boardId, swimlaneId, height);
|
||||
}
|
||||
|
||||
|
||||
// For non-logged-in users, save to localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('wekan-swimlane-heights');
|
||||
let heights = stored ? JSON.parse(stored) : {};
|
||||
|
||||
|
||||
if (!heights[boardId]) {
|
||||
heights[boardId] = {};
|
||||
}
|
||||
heights[boardId][swimlaneId] = height;
|
||||
|
||||
|
||||
localStorage.setItem('wekan-swimlane-heights', JSON.stringify(heights));
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
|
@ -1914,16 +1914,16 @@ Meteor.methods({
|
|||
if (!user) {
|
||||
throw new Meteor.Error('user-not-found', 'User not found');
|
||||
}
|
||||
|
||||
|
||||
// Check if board is already starred
|
||||
const starredBoards = (user.profile && user.profile.starredBoards) || [];
|
||||
const isStarred = starredBoards.includes(boardId);
|
||||
|
||||
|
||||
// Build update object
|
||||
const updateObject = isStarred
|
||||
const updateObject = isStarred
|
||||
? { $pull: { 'profile.starredBoards': boardId } }
|
||||
: { $addToSet: { 'profile.starredBoards': boardId } };
|
||||
|
||||
|
||||
Users.update(this.userId, updateObject);
|
||||
},
|
||||
toggleGreyIcons(value) {
|
||||
|
|
@ -1991,11 +1991,11 @@ Meteor.methods({
|
|||
check(boardId, String);
|
||||
check(spaceId, String);
|
||||
if (!this.userId) throw new Meteor.Error('not-logged-in');
|
||||
|
||||
const user = Users.findOne(this.userId);
|
||||
|
||||
const user = Users.findOne(this.userId, { fields: { 'profile.boardWorkspaceAssignments': 1 } });
|
||||
const assignments = user.profile?.boardWorkspaceAssignments || {};
|
||||
assignments[boardId] = spaceId;
|
||||
|
||||
|
||||
Users.update(this.userId, {
|
||||
$set: { 'profile.boardWorkspaceAssignments': assignments }
|
||||
});
|
||||
|
|
@ -2005,11 +2005,11 @@ Meteor.methods({
|
|||
unassignBoardFromWorkspace(boardId) {
|
||||
check(boardId, String);
|
||||
if (!this.userId) throw new Meteor.Error('not-logged-in');
|
||||
|
||||
const user = Users.findOne(this.userId);
|
||||
|
||||
const user = Users.findOne(this.userId, { fields: { 'profile.boardWorkspaceAssignments': 1 } });
|
||||
const assignments = user.profile?.boardWorkspaceAssignments || {};
|
||||
delete assignments[boardId];
|
||||
|
||||
|
||||
Users.update(this.userId, {
|
||||
$set: { 'profile.boardWorkspaceAssignments': assignments }
|
||||
});
|
||||
|
|
@ -3081,7 +3081,9 @@ if (Meteor.isServer) {
|
|||
Authentication.checkUserId(req.userId);
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: Meteor.users.find({}).map(function (doc) {
|
||||
data: Meteor.users.find({}, {
|
||||
fields: { _id: 1, username: 1 }
|
||||
}).map(function (doc) {
|
||||
return {
|
||||
_id: doc._id,
|
||||
username: doc.username,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue