Browse Source

Change list width by dragging between lists.

Thanks to xet7 !
Lauri Ojansivu 1 week ago
parent
commit
abad8cc4d5

+ 31 - 15
client/components/boards/boardBody.js

@@ -89,7 +89,9 @@ BlazeComponent.extendComponent({
 
       // Check if board needs conversion (for old structure)
       if (boardConverter.isBoardConverted(boardId)) {
-        console.log(`Board ${boardId} has already been converted, skipping conversion`);
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has already been converted, skipping conversion`);
+        }
         this.isBoardReady.set(true);
       } else {
         const needsConversion = boardConverter.needsConversion(boardId);
@@ -127,7 +129,9 @@ BlazeComponent.extendComponent({
         if (error) {
           console.error('Failed to start background migration:', error);
         } else {
-          console.log('Background migration started for board:', boardId);
+          if (process.env.DEBUG === 'true') {
+            console.log('Background migration started for board:', boardId);
+          }
         }
       });
     } catch (error) {
@@ -139,7 +143,9 @@ BlazeComponent.extendComponent({
     try {
       // Check if board has already been migrated
       if (attachmentMigrationManager.isBoardMigrated(boardId)) {
-        console.log(`Board ${boardId} has already been migrated, skipping`);
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has already been migrated, skipping`);
+        }
         return;
       }
 
@@ -147,12 +153,16 @@ BlazeComponent.extendComponent({
       const unconvertedAttachments = attachmentMigrationManager.getUnconvertedAttachments(boardId);
       
       if (unconvertedAttachments.length > 0) {
-        console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
+        if (process.env.DEBUG === 'true') {
+          console.log(`Starting attachment migration for ${unconvertedAttachments.length} attachments in board ${boardId}`);
+        }
         await attachmentMigrationManager.startAttachmentMigration(boardId);
       } else {
         // No attachments to migrate, mark board as migrated
         // This will be handled by the migration manager itself
-        console.log(`Board ${boardId} has no attachments to migrate`);
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has no attachments to migrate`);
+        }
       }
     } catch (error) {
       console.error('Error starting attachment migration:', error);
@@ -622,14 +632,18 @@ BlazeComponent.extendComponent({
   hasSwimlanes() {
     const currentBoard = Utils.getCurrentBoard();
     if (!currentBoard) {
-      console.log('hasSwimlanes: No current board');
+      if (process.env.DEBUG === 'true') {
+        console.log('hasSwimlanes: No current board');
+      }
       return false;
     }
     
     try {
       const swimlanes = currentBoard.swimlanes();
       const hasSwimlanes = swimlanes && swimlanes.length > 0;
-      console.log('hasSwimlanes: Board has', swimlanes ? swimlanes.length : 0, 'swimlanes');
+      if (process.env.DEBUG === 'true') {
+        console.log('hasSwimlanes: Board has', swimlanes ? swimlanes.length : 0, 'swimlanes');
+      }
       return hasSwimlanes;
     } catch (error) {
       console.error('hasSwimlanes: Error getting swimlanes:', error);
@@ -661,14 +675,16 @@ BlazeComponent.extendComponent({
     const isMigrating = this.isMigrating.get();
     const boardView = Utils.boardView();
     
-    console.log('=== BOARD DEBUG STATE ===');
-    console.log('currentBoardId:', currentBoardId);
-    console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
-    console.log('isBoardReady:', isBoardReady);
-    console.log('isConverting:', isConverting);
-    console.log('isMigrating:', isMigrating);
-    console.log('boardView:', boardView);
-    console.log('========================');
+    if (process.env.DEBUG === 'true') {
+      console.log('=== BOARD DEBUG STATE ===');
+      console.log('currentBoardId:', currentBoardId);
+      console.log('currentBoard:', !!currentBoard, currentBoard ? currentBoard.title : 'none');
+      console.log('isBoardReady:', isBoardReady);
+      console.log('isConverting:', isConverting);
+      console.log('isMigrating:', isMigrating);
+      console.log('boardView:', boardView);
+      console.log('========================');
+    }
     
     return {
       currentBoardId,

+ 213 - 0
client/components/lists/list.css

@@ -8,6 +8,219 @@
   padding: 0;
   float: left;
 }
+
+/* List resize handle */
+.list-resize-handle {
+  position: absolute;
+  top: 0;
+  right: -3px;
+  width: 6px;
+  height: 100%;
+  cursor: col-resize;
+  z-index: 10;
+  background: transparent;
+  transition: background-color 0.2s ease;
+  border-radius: 2px;
+}
+
+.list-resize-handle:hover {
+  background: rgba(0, 123, 255, 0.4);
+  box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
+}
+
+.list-resize-handle:active {
+  background: rgba(0, 123, 255, 0.6);
+  box-shadow: 0 0 6px rgba(0, 123, 255, 0.4);
+}
+
+/* Show resize handle only on hover */
+.list:hover .list-resize-handle {
+  background: rgba(0, 0, 0, 0.1);
+}
+
+.list:hover .list-resize-handle:hover {
+  background: rgba(0, 123, 255, 0.4);
+  box-shadow: 0 0 4px rgba(0, 123, 255, 0.3);
+}
+
+/* Add a subtle indicator line */
+.list-resize-handle::before {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 2px;
+  height: 20px;
+  background: rgba(0, 0, 0, 0.2);
+  border-radius: 1px;
+  opacity: 0;
+  transition: opacity 0.2s ease;
+}
+
+.list-resize-handle:hover::before {
+  opacity: 1;
+}
+
+/* Disable resize handle for collapsed lists and mobile view */
+.list.list-collapsed .list-resize-handle,
+.list.mobile-view .list-resize-handle {
+  display: none;
+}
+
+/* Disable resize handle for auto-width lists */
+.list.list-auto-width .list-resize-handle {
+  display: none;
+}
+
+/* Visual feedback during resize */
+.list.list-resizing {
+  transition: none !important;
+  box-shadow: 0 0 10px rgba(0, 123, 255, 0.3);
+  /* Ensure the list maintains its new width during resize */
+  flex: none !important;
+  flex-basis: auto !important;
+  flex-grow: 0 !important;
+  flex-shrink: 0 !important;
+  /* Override any conflicting layout properties */
+  float: left !important;
+  display: block !important;
+  position: relative !important;
+  /* Force width to be respected */
+  width: var(--list-width, auto) !important;
+  min-width: var(--list-width, auto) !important;
+  max-width: var(--list-width, auto) !important;
+}
+
+body.list-resizing-active {
+  cursor: col-resize !important;
+}
+
+body.list-resizing-active * {
+  cursor: col-resize !important;
+}
+
+/* Ensure swimlane container doesn't interfere with list resizing */
+.swimlane .list.list-resizing {
+  /* Override any swimlane flex properties */
+  flex: none !important;
+  flex-basis: auto !important;
+  flex-grow: 0 !important;
+  flex-shrink: 0 !important;
+  /* Ensure width is respected */
+  width: var(--list-width, auto) !important;
+  min-width: var(--list-width, auto) !important;
+  max-width: var(--list-width, auto) !important;
+}
+
+/* More aggressive override for any container that might interfere */
+.js-swimlane .list.list-resizing,
+.dragscroll .list.list-resizing,
+[id^="swimlane-"] .list.list-resizing {
+  /* Force the width to be applied */
+  width: var(--list-width, auto) !important;
+  min-width: var(--list-width, auto) !important;
+  max-width: var(--list-width, auto) !important;
+  flex: none !important;
+  flex-basis: auto !important;
+  flex-grow: 0 !important;
+  flex-shrink: 0 !important;
+  float: left !important;
+  display: block !important;
+}
+
+/* Ensure the width persists after resize is complete */
+.js-swimlane .list[style*="--list-width"],
+.dragscroll .list[style*="--list-width"],
+[id^="swimlane-"] .list[style*="--list-width"] {
+  /* Maintain the width after resize */
+  width: var(--list-width, auto) !important;
+  min-width: var(--list-width, auto) !important;
+  max-width: var(--list-width, auto) !important;
+  flex: none !important;
+  flex-basis: auto !important;
+  flex-grow: 0 !important;
+  flex-shrink: 0 !important;
+  float: left !important;
+  display: block !important;
+}
+
+/* Ensure consistent header height for all lists */
+.list-header {
+  /* Maintain consistent height and padding for all lists */
+  min-height: 2.5vh !important;
+  height: auto !important;
+  padding: 2.5vh 1.5vw 0.5vh !important;
+  /* Make sure the background covers the full height */
+  background-color: #e4e4e4 !important;
+  border-bottom: 0.8vh solid #e4e4e4 !important;
+  /* Use original display for consistent button positioning */
+  display: block !important;
+  position: relative !important;
+  /* Prevent vertical expansion but allow normal height */
+  overflow: hidden !important;
+}
+
+/* Ensure title text doesn't cause height changes for all lists */
+.list-header .list-header-name {
+  /* Prevent text wrapping to maintain consistent height */
+  white-space: nowrap !important;
+  /* Truncate text with ellipsis if too long */
+  text-overflow: ellipsis !important;
+  /* Ensure proper line height */
+  line-height: 1.2 !important;
+  /* Ensure it doesn't overflow */
+  overflow: hidden !important;
+  /* Add margin to prevent overlap with buttons */
+  margin-right: 120px !important;
+}
+
+/* Position drag handle at top-right corner for ALL lists */
+.list-header .list-header-handle {
+  /* Position at top-right corner, aligned with title text top */
+  position: absolute !important;
+  top: 2.5vh !important;
+  right: 1.5vw !important;
+  /* Ensure it's above other elements */
+  z-index: 15 !important;
+  /* Remove margin since it's absolutely positioned */
+  margin-right: 0 !important;
+  /* Ensure proper display */
+  display: inline-block !important;
+}
+
+/* Ensure buttons maintain original positioning */
+.js-swimlane .list[style*="--list-width"] .list-header .list-header-plus-top,
+.js-swimlane .list[style*="--list-width"] .list-header .js-collapse,
+.js-swimlane .list[style*="--list-width"] .list-header .js-open-list-menu,
+.dragscroll .list[style*="--list-width"] .list-header .list-header-plus-top,
+.dragscroll .list[style*="--list-width"] .list-header .js-collapse,
+.dragscroll .list[style*="--list-width"] .list-header .js-open-list-menu,
+[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-plus-top,
+[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-collapse,
+[id^="swimlane-"] .list[style*="--list-width"] .list-header .js-open-list-menu {
+  /* Use original positioning to maintain layout */
+  position: relative !important;
+  /* Maintain original spacing */
+  margin-right: 15px !important;
+  /* Ensure proper display */
+  display: inline-block !important;
+}
+
+/* Ensure watch icon and card count maintain original positioning */
+.js-swimlane .list[style*="--list-width"] .list-header .list-header-watch-icon,
+.dragscroll .list[style*="--list-width"] .list-header .list-header-watch-icon,
+[id^="swimlane-"] .list[style*="--list-width"] .list-header .list-header-watch-icon,
+.js-swimlane .list[style*="--list-width"] .list-header .cardCount,
+.dragscroll .list[style*="--list-width"] .list-header .cardCount,
+[id^="swimlane-"] .list[style*="--list-width"] .list-header .cardCount {
+  /* Use original positioning to maintain layout */
+  position: relative !important;
+  /* Maintain original spacing */
+  margin-right: 15px !important;
+  /* Ensure proper display */
+  display: inline-block !important;
+}
 [id^="swimlane-"] .list:first-child {
   min-width: 2.5vw;
 }

+ 1 - 0
client/components/lists/list.jade

@@ -4,6 +4,7 @@ template(name='list')
                 class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
     +listHeader
     +listBody
+    .list-resize-handle.js-list-resize-handle
 
 template(name='miniList')
   a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}")

+ 143 - 0
client/components/lists/list.js

@@ -24,6 +24,9 @@ BlazeComponent.extendComponent({
   onRendered() {
     const boardComponent = this.parentComponent().parentComponent();
 
+    // Initialize list resize functionality
+    this.initializeListResize();
+
     const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
     const $cards = this.$('.js-minicards');
 
@@ -212,6 +215,146 @@ BlazeComponent.extendComponent({
     const list = Template.currentData();
     return user.isAutoWidth(list.boardId);
   },
+
+  initializeListResize() {
+    const list = Template.currentData();
+    const $list = this.$('.js-list');
+    const $resizeHandle = this.$('.js-list-resize-handle');
+    
+    // Only enable resize for non-collapsed, non-auto-width lists
+    if (list.collapsed || this.autoWidth()) {
+      $resizeHandle.hide();
+      return;
+    }
+
+    let isResizing = false;
+    let startX = 0;
+    let startWidth = 0;
+    let minWidth = 100; // Minimum width as defined in the existing code
+    let maxWidth = this.listConstraint() || 1000; // Use constraint as max width
+    let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
+    const component = this; // Store reference to component for use in event handlers
+
+    const startResize = (e) => {
+      isResizing = true;
+      startX = e.pageX || e.originalEvent.touches[0].pageX;
+      startWidth = $list.outerWidth();
+      
+      // Add visual feedback
+      $list.addClass('list-resizing');
+      $('body').addClass('list-resizing-active');
+      
+      // Prevent text selection during resize
+      $('body').css('user-select', 'none');
+      
+      e.preventDefault();
+    };
+
+    const doResize = (e) => {
+      if (!isResizing) return;
+      
+      const currentX = e.pageX || e.originalEvent.touches[0].pageX;
+      const deltaX = currentX - startX;
+      const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
+      
+      // Apply the new width immediately for real-time feedback using CSS custom properties
+      $list[0].style.setProperty('--list-width', `${newWidth}px`);
+      $list.css({
+        'width': `${newWidth}px`,
+        'min-width': `${newWidth}px`,
+        'max-width': `${newWidth}px`,
+        'flex': 'none',
+        'flex-basis': 'auto',
+        'flex-grow': '0',
+        'flex-shrink': '0'
+      });
+      
+      // Debug: log the width change
+      if (process.env.DEBUG === 'true') {
+        console.log(`Resizing list to ${newWidth}px`);
+      }
+      
+      e.preventDefault();
+    };
+
+    const stopResize = (e) => {
+      if (!isResizing) return;
+      
+      isResizing = false;
+      
+      // Calculate final width
+      const currentX = e.pageX || e.originalEvent.touches[0].pageX;
+      const deltaX = currentX - startX;
+      const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
+      
+      // Ensure the final width is applied using CSS custom properties
+      $list[0].style.setProperty('--list-width', `${finalWidth}px`);
+      $list.css({
+        'width': `${finalWidth}px`,
+        'min-width': `${finalWidth}px`,
+        'max-width': `${finalWidth}px`,
+        'flex': 'none',
+        'flex-basis': 'auto',
+        'flex-grow': '0',
+        'flex-shrink': '0'
+      });
+      
+      // Remove visual feedback but keep the width
+      $list.removeClass('list-resizing');
+      $('body').removeClass('list-resizing-active');
+      $('body').css('user-select', '');
+      
+      // Keep the CSS custom property for persistent width
+      // The CSS custom property will remain on the element to maintain the width
+      
+      // Save the new width using the existing system
+      const boardId = list.boardId;
+      const listId = list._id;
+      
+      // Use the same method as the hamburger menu
+      if (process.env.DEBUG === 'true') {
+        console.log(`Saving list width: ${finalWidth}px for list ${listId}`);
+      }
+      Meteor.call('applyListWidth', boardId, listId, finalWidth, listConstraint, (error, result) => {
+        if (error) {
+          console.error('Error saving list width:', error);
+        } else {
+          if (process.env.DEBUG === 'true') {
+            console.log('List width saved successfully:', result);
+          }
+        }
+      });
+      
+      e.preventDefault();
+    };
+
+    // Mouse events
+    $resizeHandle.on('mousedown', startResize);
+    $(document).on('mousemove', doResize);
+    $(document).on('mouseup', stopResize);
+    
+    // Touch events for mobile
+    $resizeHandle.on('touchstart', startResize);
+    $(document).on('touchmove', doResize);
+    $(document).on('touchend', stopResize);
+    
+    // Reactively update resize handle visibility when auto-width changes
+    component.autorun(() => {
+      if (component.autoWidth()) {
+        $resizeHandle.hide();
+      } else {
+        $resizeHandle.show();
+      }
+    });
+
+    // Clean up on component destruction
+    component.onDestroyed(() => {
+      $(document).off('mousemove', doResize);
+      $(document).off('mouseup', stopResize);
+      $(document).off('touchmove', doResize);
+      $(document).off('touchend', stopResize);
+    });
+  },
 }).register('list');
 
 Template.miniList.events({

+ 15 - 5
client/lib/attachmentMigrationManager.js

@@ -103,14 +103,18 @@ class AttachmentMigrationManager {
 
     // Check if this board has already been migrated (client-side check first)
     if (globalMigratedBoards.has(boardId)) {
-      console.log(`Board ${boardId} has already been migrated (client-side), skipping`);
+      if (process.env.DEBUG === 'true') {
+        console.log(`Board ${boardId} has already been migrated (client-side), skipping`);
+      }
       return;
     }
 
     // Double-check with server-side check
     const serverMigrated = await this.isBoardMigratedServer(boardId);
     if (serverMigrated) {
-      console.log(`Board ${boardId} has already been migrated (server-side), skipping`);
+      if (process.env.DEBUG === 'true') {
+        console.log(`Board ${boardId} has already been migrated (server-side), skipping`);
+      }
       globalMigratedBoards.add(boardId); // Sync client-side tracking
       return;
     }
@@ -128,7 +132,9 @@ class AttachmentMigrationManager {
         attachmentMigrationProgress.set(100);
         isMigratingAttachments.set(false);
         globalMigratedBoards.add(boardId); // Mark board as migrated
-        console.log(`Board ${boardId} has no attachments to migrate, marked as migrated`);
+        if (process.env.DEBUG === 'true') {
+          console.log(`Board ${boardId} has no attachments to migrate, marked as migrated`);
+        }
         return;
       }
 
@@ -140,7 +146,9 @@ class AttachmentMigrationManager {
           attachmentMigrationStatus.set(`Migration failed: ${errorMessage}`);
           isMigratingAttachments.set(false);
         } else {
-          console.log('Attachment migration started for board:', boardId);
+          if (process.env.DEBUG === 'true') {
+            console.log('Attachment migration started for board:', boardId);
+          }
           this.pollAttachmentMigrationProgress(boardId);
         }
       });
@@ -177,7 +185,9 @@ class AttachmentMigrationManager {
             isMigratingAttachments.set(false);
             this.migrationCache.clear(); // Clear cache to refresh data
             globalMigratedBoards.add(boardId); // Mark board as migrated
-            console.log(`Board ${boardId} migration completed and marked as migrated`);
+            if (process.env.DEBUG === 'true') {
+              console.log(`Board ${boardId} migration completed and marked as migrated`);
+            }
           }
         }
       });

+ 5 - 5
models/cards.js

@@ -1764,11 +1764,11 @@ Cards.helpers({
   },
 
   setTitle(title) {
-    // Sanitize title on client side as well
+    // Basic client-side validation - server will handle full sanitization
     let sanitizedTitle = title;
     if (typeof title === 'string') {
-      const { sanitizeTitle } = require('../server/lib/inputSanitizer');
-      sanitizedTitle = sanitizeTitle(title);
+      // Basic length check to prevent abuse
+      sanitizedTitle = title.length > 1000 ? title.substring(0, 1000) : title;
       if (process.env.DEBUG === 'true' && sanitizedTitle !== title) {
         console.warn('Client-side sanitized card title:', title, '->', sanitizedTitle);
       }
@@ -3583,8 +3583,8 @@ JsonRoutes.add('GET', '/api/boards/:boardId/cards_count', function(
       Authentication.checkBoardAccess(req.userId, paramBoardId);
 
       if (req.body.title) {
-        const { sanitizeTitle } = require('../server/lib/inputSanitizer');
-        const newTitle = sanitizeTitle(req.body.title);
+        // Basic client-side validation - server will handle full sanitization
+        const newTitle = req.body.title.length > 1000 ? req.body.title.substring(0, 1000) : req.body.title;
 
         if (process.env.DEBUG === 'true' && newTitle !== req.body.title) {
           console.warn('Sanitized card title input:', req.body.title, '->', newTitle);

+ 5 - 8
models/lists.js

@@ -314,13 +314,10 @@ Lists.helpers({
 
 Lists.mutations({
   rename(title) {
-    // Sanitize title on client side as well
+    // Basic client-side validation - server will handle full sanitization
     if (typeof title === 'string') {
-      const { sanitizeTitle } = require('../server/lib/inputSanitizer');
-      const sanitizedTitle = sanitizeTitle(title);
-      if (process.env.DEBUG === 'true' && sanitizedTitle !== title) {
-        console.warn('Client-side sanitized list title:', title, '->', sanitizedTitle);
-      }
+      // Basic length check to prevent abuse
+      const sanitizedTitle = title.length > 1000 ? title.substring(0, 1000) : title;
       return { $set: { title: sanitizedTitle } };
     }
     return { $set: { title } };
@@ -654,8 +651,8 @@ if (Meteor.isServer) {
 
       // Update title if provided
       if (req.body.title) {
-        const { sanitizeTitle } = require('../server/lib/inputSanitizer');
-        const newTitle = sanitizeTitle(req.body.title);
+        // Basic client-side validation - server will handle full sanitization
+        const newTitle = req.body.title.length > 1000 ? req.body.title.substring(0, 1000) : req.body.title;
 
         if (process.env.DEBUG === 'true' && newTitle !== req.body.title) {
           console.warn('Sanitized list title input:', req.body.title, '->', newTitle);