Merge branch 'main' into feature/reactive-cache-async-migration

This commit is contained in:
Harry Adel 2026-02-17 16:45:06 +02:00
commit 5212f3beb3
328 changed files with 15124 additions and 3392 deletions

View file

@ -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');

View file

@ -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');

View file

@ -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}`;
},

View file

@ -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,

View file

@ -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++;

View file

@ -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) {

View file

@ -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}`);

View file

@ -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) {

View file

@ -225,6 +225,8 @@ Lists.helpers({
for (const card of cards) {
await card.copy(boardId, swimlaneId, _id);
}
return _id;
},
async move(boardId, swimlaneId) {

View file

@ -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
};
}
});

View file

@ -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,

View file

@ -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}`;
},
});

View file

@ -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 };
},

View file

@ -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,