diff --git a/CHANGELOG.md b/CHANGELOG.md index c4fbf8fe7..2a968fb17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -192,7 +192,7 @@ and adds the following new features: - [Mobile one board per row. Board zoom size percent. Board toggle mobile/desktop mode. In Progress](https://github.com/wekan/wekan/commit/752699d1c2fb8ea9ff0f3ec9ae0b2b776443d826). Thanks to xet7. -- [Drag any files from file manager to minicard or opened card. +- Drag any files from file manager to minicard or opened card. [Part 1](https://github.com/wekan/wekan/commit/3e9481c5bd2c02ba501bd0a6ef1d1e6ce82bb1d9), [Part 2](https://github.com/wekan/wekan/commit/cdd7d69c660d0b6ac06b7b75d4f59985b8a9322a). Thanks to xet7. diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index 7a53192ce..686fd5eac 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -5,11 +5,11 @@ template(name="minicard") class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}") if canModifyCard if isTouchScreenOrShowDesktopDragHandles - a.fa.fa-navicon.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") + a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰ .handle - .fa.fa-arrows + | ↔️ else - a.fa.fa-navicon.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") + a.minicard-details-menu.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}") | ☰ .dates if getReceived unless getStart @@ -36,7 +36,7 @@ template(name="minicard") if hasActiveUploads .minicard-upload-progress .upload-progress-header - i.fa.fa-upload + | 📤 span {{_ 'uploading-files'}} ({{uploadCount}}) each uploads .upload-progress-item(class="{{#if $eq status 'error'}}upload-error{{/if}}") @@ -45,11 +45,11 @@ template(name="minicard") .upload-progress-fill(style="width: {{progress}}%") if $eq status 'error' .upload-progress-error - i.fa.fa-exclamation-triangle + | ⚠️ span {{_ 'upload-failed'}} else if $eq status 'completed' .upload-progress-success - i.fa.fa-check + | ✅ span {{_ 'upload-completed'}} .minicard-title @@ -61,12 +61,12 @@ template(name="minicard") | {{ parentCardName }} if isLinkedBoard a.js-linked-link - span.linked-icon.fa.fa-folder + span.linked-icon | 📁 else if isLinkedCard a.js-linked-link - span.linked-icon.fa.fa-id-card + span.linked-icon | 🃏 if getArchived - span.linked-icon.linked-archived.fa.fa-archive + span.linked-icon.linked-archived | 📦 +viewer if currentBoard.allowsCardNumber span.card-number @@ -147,7 +147,7 @@ template(name="minicard") if canModifyCard if comments.length .badge(title="{{_ 'card-comments-title' comments.length }}") - span.badge-icon.fa.fa-comment-o.badge-comment.badge-text + span.badge-icon.badge-comment.badge-text | 💬 = ' ' = comments.length //span.badge-comment.badge-text @@ -155,36 +155,36 @@ template(name="minicard") if getDescription unless currentBoard.allowsDescriptionTextOnMinicard .badge.badge-state-image-only(title=getDescription) - span.badge-icon.fa.fa-align-left + span.badge-icon | 📝 if getVoteQuestion .badge.badge-state-image-only(title=getVoteQuestion) - span.badge-icon.fa.fa-thumbs-up(class="{{#if voteState}}text-green{{/if}}") + span.badge-icon(class="{{#if voteState}}text-green{{/if}}") | 👍 span.badge-text {{ voteCountPositive }} - span.badge-icon.fa.fa-thumbs-down(class="{{#if $eq voteState false}}text-red{{/if}}") + span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}") | 👎 span.badge-text {{ voteCountNegative }} if getPokerQuestion .badge.badge-state-image-only(title=getPokerQuestion) - span.badge-icon.fa.fa-check(class="{{#if pokerState}}text-green{{/if}}") + span.badge-icon(class="{{#if pokerState}}text-green{{/if}}") | ✅ if expiredPoker span.badge-text {{ getPokerEstimation }} if attachments.length if currentBoard.allowsBadgeAttachmentOnMinicard .badge - span.badge-icon.fa.fa-paperclip + span.badge-icon | 📎 span.badge-text= attachments.length if checklists.length .badge(class="{{#if checklistFinished}}is-finished{{/if}}") - span.badge-icon.fa.fa-check-square-o + span.badge-icon | ☑️ span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}} if allSubtasks.count .badge - span.badge-icon.fa.fa-sitemap + span.badge-icon | 🌐 span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}} //{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down if currentBoard.allowsCardSortingByNumber if currentBoard.allowsCardSortingByNumberOnMinicard .badge - span.badge-icon.fa.fa-sort + span.badge-icon | 🔢 span.badge-text.check-list-sort {{ sort }} if currentBoard.allowsDescriptionTextOnMinicard if getDescription @@ -193,7 +193,7 @@ template(name="minicard") | {{ getDescription }} if shouldShowListOnMinicard .minicard-list-name - i.fa.fa-list + | 📋 | {{ listName }} if $eq 'subtext-with-full-path' currentBoard.presentParentTask .parent-subtext @@ -212,50 +212,50 @@ template(name="minicardDetailsActionsPopup") if canModifyCard li a.js-move-card - i.fa.fa-arrow-right + | ➡️ | {{_ 'moveCardPopup-title'}} li a.js-copy-card - i.fa.fa-copy + | 📋 | {{_ 'copyCardPopup-title'}} hr li a.js-archive - i.fa.fa-arrow-right - i.fa.fa-archive + | ➡️ + | 📦 | {{_ 'archive-card'}} hr li a.js-move-card-to-top - i.fa.fa-arrow-up + | ⬆️ | {{_ 'moveCardToTop-title'}} li a.js-move-card-to-bottom - i.fa.fa-arrow-down + | ⬇️ | {{_ 'moveCardToBottom-title'}} hr li a.js-add-labels - i.fa.fa-tags + | 🏷️ | {{_ 'card-edit-labels'}} li a.js-due-date - i.fa.fa-sign-in + | 📥 | {{_ 'editCardDueDatePopup-title'}} li a.js-set-card-color - i.fa.fa-paint-brush + | 🎨 | {{_ 'setCardColorPopup-title'}} li a.js-link - i.fa.fa-link + | 🔗 | {{_ 'link-card'}} li a.js-toggle-watch-card if isWatching - i.fa.fa-eye + | 👁️ | {{_ 'unwatch'}} else - i.fa.fa-eye-slash + | 👁️-slash | {{_ 'watch'}} diff --git a/client/components/lists/list.css b/client/components/lists/list.css index 669707500..8b76046ad 100644 --- a/client/components/lists/list.css +++ b/client/components/lists/list.css @@ -21,6 +21,8 @@ background: transparent; transition: background-color 0.2s ease; border-radius: 2px; + /* Ensure the handle is clickable */ + pointer-events: auto; } .list-resize-handle:hover { @@ -90,6 +92,8 @@ width: var(--list-width, auto) !important; min-width: var(--list-width, auto) !important; max-width: var(--list-width, auto) !important; + /* Ensure the width is applied immediately */ + overflow: visible !important; } body.list-resizing-active { @@ -250,7 +254,70 @@ body.list-resizing-active * { } .list.list-collapsed { flex: none; + min-width: 60px; + max-width: 80px; + width: 60px; + min-height: 60vh; + height: 60vh; + overflow: visible; + position: relative; } +.list.list-collapsed .list-header { + padding: 1vh 1.5vw 0.5vh; + min-height: 2.5vh !important; + height: auto !important; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + position: relative; + overflow: visible !important; + width: 100%; + max-width: 60px; + margin: 0 auto; +} +.list.list-collapsed .list-header .js-collapse { + margin: 0 auto 20px auto; + z-index: 10; + padding: 8px 12px; + font-size: 12px; + white-space: nowrap; + display: block; + width: fit-content; +} +.list.list-collapsed .list-header .list-rotated { + width: auto !important; + height: auto !important; + margin: 20px 0 0 0 !important; + position: relative !important; + overflow: visible !important; +} + +.list.list-collapsed .list-header .list-rotated h2.list-header-name { + text-align: left; + overflow: visible; + white-space: nowrap; + display: block !important; + font-size: 12px; + line-height: 1.2; + color: #333; + background-color: rgba(255, 255, 255, 0.95); + border: 1px solid #ddd; + padding: 8px 4px; + border-radius: 4px; + margin: 0 auto; + width: 25vh; + height: 60vh; + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% + 50px), -50%) rotate(0deg); + z-index: 10; + visibility: visible !important; + opacity: 1 !important; + pointer-events: none; +} + .list.list-composer .open-list-composer, .list .list-composer .open-list-composer { color: #8c8c8c; @@ -334,11 +401,152 @@ body.list-resizing-active * { color: #a6a6a6; margin-right: 15px; } -.list-header .list-header-uncollapse-left { +.list-header .js-collapse { color: #a6a6a6; + margin-right: 15px; + display: inline-block; + vertical-align: middle; + padding: 5px 8px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f5f5f5; + cursor: pointer; + font-size: 14px; } -.list-header .list-header-uncollapse-right { - color: #a6a6a6; +.list-header .js-collapse:hover { + background-color: #e0e0e0; + color: #333; +} +.list.list-collapsed .list-header .js-collapse { + display: inline-block !important; + visibility: visible !important; + opacity: 1 !important; +} + +/* Responsive adjustments for collapsed lists */ +@media (min-width: 768px) { + .list.list-collapsed { + min-width: 60px; + max-width: 80px; + width: 60px; + min-height: 60vh; + height: 60vh; + } + .list.list-collapsed .list-header { + max-width: 60px; + margin: 0 auto; + min-height: 2.5vh !important; + height: auto !important; + } + .list.list-collapsed .list-header .list-rotated { + width: auto !important; + height: auto !important; + margin: 20px 0 0 0 !important; + position: relative !important; + } + .list.list-collapsed .list-header .list-rotated h2.list-header-name { + width: 15vh; + font-size: 12px; + height: 30px; + line-height: 1.2; + padding: 8px 4px; + margin: 0 auto; + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% + 50px), -50%) rotate(0deg); + text-align: left; + visibility: visible !important; + opacity: 1 !important; + display: block !important; + background-color: rgba(255, 255, 255, 0.95); + border: 1px solid #ddd; + color: #333; + z-index: 10; + } + .list.list-collapsed .list-header .js-collapse { + margin: 0 auto 20px auto; + } +} + +@media (min-width: 1024px) { + .list.list-collapsed { + min-height: 60vh; + height: 60vh; + } + .list.list-collapsed .list-header { + min-height: 2.5vh !important; + height: auto !important; + } + .list.list-collapsed .list-header .list-rotated { + width: auto !important; + height: auto !important; + margin: 20px 0 0 0 !important; + position: relative !important; + } + .list.list-collapsed .list-header .list-rotated h2.list-header-name { + width: 15vh; + font-size: 12px; + height: 30px; + line-height: 1.2; + padding: 8px 4px; + margin: 0 auto; + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% + 50px), -50%) rotate(0deg); + text-align: left; + visibility: visible !important; + opacity: 1 !important; + display: block !important; + background-color: rgba(255, 255, 255, 0.95); + border: 1px solid #ddd; + color: #333; + z-index: 10; + } + .list.list-collapsed .list-header .js-collapse { + margin: 0 auto 20px auto; + } +} + +@media (min-width: 1200px) { + .list.list-collapsed { + min-height: 60vh; + height: 60vh; + } + .list.list-collapsed .list-header { + min-height: 2.5vh !important; + height: auto !important; + } + .list.list-collapsed .list-header .list-rotated { + width: auto !important; + height: auto !important; + margin: 20px 0 0 0 !important; + position: relative !important; + } + .list.list-collapsed .list-header .list-rotated h2.list-header-name { + width: 15vh; + font-size: 12px; + height: 30px; + line-height: 1.2; + padding: 8px 4px; + margin: 0 auto; + position: absolute; + left: 50%; + top: 50%; + transform: translate(calc(-50% + 50px), -50%) rotate(0deg); + text-align: left; + visibility: visible !important; + opacity: 1 !important; + display: block !important; + background-color: rgba(255, 255, 255, 0.95); + border: 1px solid #ddd; + color: #333; + z-index: 10; + } + .list.list-collapsed .list-header .js-collapse { + margin: 0 auto 20px auto; + } } .list-header .list-header-collapse { color: #a6a6a6; diff --git a/client/components/lists/list.jade b/client/components/lists/list.jade index 7484d0a00..eed4d67f9 100644 --- a/client/components/lists/list.jade +++ b/client/components/lists/list.jade @@ -4,7 +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 + .list-resize-handle.js-list-resize-handle.nodragscroll template(name='miniList') a.mini-list.js-select-list.js-list(id="js-list-{{_id}}" class="{{#if isMiniScreen}}mobile-view{{/if}}") diff --git a/client/components/lists/list.js b/client/components/lists/list.js index 5d46d9127..6c3695ebf 100644 --- a/client/components/lists/list.js +++ b/client/components/lists/list.js @@ -24,7 +24,7 @@ BlazeComponent.extendComponent({ onRendered() { const boardComponent = this.parentComponent().parentComponent(); - // Initialize list resize functionality + // Initialize list resize functionality immediately this.initializeListResize(); const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)'; @@ -201,13 +201,15 @@ BlazeComponent.extendComponent({ listWidth() { const user = ReactiveCache.getCurrentUser(); const list = Template.currentData(); - return user.getListWidth(list.boardId, list._id); + if (!user || !list) return 270; // Return default width if user or list is not available + return user.getListWidthFromStorage(list.boardId, list._id); }, listConstraint() { const user = ReactiveCache.getCurrentUser(); const list = Template.currentData(); - return user.getListConstraint(list.boardId, list._id); + if (!user || !list) return 550; // Return default constraint if user or list is not available + return user.getListConstraintFromStorage(list.boardId, list._id); }, autoWidth() { @@ -217,12 +219,31 @@ BlazeComponent.extendComponent({ }, initializeListResize() { + // Check if we're still in a valid template context + if (!Template.currentData()) { + console.warn('No current template data available for list resize initialization'); + return; + } + const list = Template.currentData(); const $list = this.$('.js-list'); const $resizeHandle = this.$('.js-list-resize-handle'); + // Check if elements exist + if (!$list.length || !$resizeHandle.length) { + console.warn('List or resize handle not found, retrying in 100ms'); + Meteor.setTimeout(() => { + if (!this.isDestroyed) { + this.initializeListResize(); + } + }, 100); + return; + } + + // Only enable resize for non-collapsed, non-auto-width lists - if (list.collapsed || this.autoWidth()) { + const isAutoWidth = this.autoWidth(); + if (list.collapsed || isAutoWidth) { $resizeHandle.hide(); return; } @@ -240,41 +261,41 @@ BlazeComponent.extendComponent({ 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(); + e.stopPropagation(); }; const doResize = (e) => { - if (!isResizing) return; + 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 + // Apply the new width immediately for real-time feedback $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' - }); + $list[0].style.setProperty('width', `${newWidth}px`); + $list[0].style.setProperty('min-width', `${newWidth}px`); + $list[0].style.setProperty('max-width', `${newWidth}px`); + $list[0].style.setProperty('flex', 'none'); + $list[0].style.setProperty('flex-basis', 'auto'); + $list[0].style.setProperty('flex-grow', '0'); + $list[0].style.setProperty('flex-shrink', '0'); - // Debug: log the width change - if (process.env.DEBUG === 'true') { - console.log(`Resizing list to ${newWidth}px`); - } e.preventDefault(); + e.stopPropagation(); }; const stopResize = (e) => { @@ -287,17 +308,15 @@ BlazeComponent.extendComponent({ const deltaX = currentX - startX; const finalWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX)); - // Ensure the final width is applied using CSS custom properties + // Ensure the final width is applied $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' - }); + $list[0].style.setProperty('width', `${finalWidth}px`); + $list[0].style.setProperty('min-width', `${finalWidth}px`); + $list[0].style.setProperty('max-width', `${finalWidth}px`); + $list[0].style.setProperty('flex', 'none'); + $list[0].style.setProperty('flex-basis', 'auto'); + $list[0].style.setProperty('flex-grow', '0'); + $list[0].style.setProperty('flex-shrink', '0'); // Remove visual feedback but keep the width $list.removeClass('list-resizing'); @@ -311,16 +330,14 @@ BlazeComponent.extendComponent({ const boardId = list.boardId; const listId = list._id; - // Use the same method as the hamburger menu + // Use the new storage method that handles both logged-in and non-logged-in users if (process.env.DEBUG === 'true') { - console.log(`Saving list width: ${finalWidth}px for list ${listId}`); } - Meteor.call('applyListWidth', boardId, listId, finalWidth, listConstraint, (error, result) => { + Meteor.call('applyListWidthToStorage', 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); } } }); @@ -334,9 +351,16 @@ BlazeComponent.extendComponent({ $(document).on('mouseup', stopResize); // Touch events for mobile - $resizeHandle.on('touchstart', startResize); - $(document).on('touchmove', doResize); - $(document).on('touchend', stopResize); + $resizeHandle.on('touchstart', startResize, { passive: false }); + $(document).on('touchmove', doResize, { passive: false }); + $(document).on('touchend', stopResize, { passive: false }); + + + // Prevent dragscroll interference + $resizeHandle.on('mousedown', (e) => { + e.stopPropagation(); + }); + // Reactively update resize handle visibility when auto-width changes component.autorun(() => { diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade index 662b5f187..e08684a4f 100644 --- a/client/components/lists/listBody.jade +++ b/client/components/lists/listBody.jade @@ -32,7 +32,7 @@ template(name="listBody") +addCardForm(listId=_id position="bottom") else a.open-minicard-composer.js-card-composer.js-open-inlined-form(title="{{_ 'add-card-to-bottom-of-list'}}") - i.fa.fa-plus + | ➕ template(name="spinnerList") .sk-spinner.sk-spinner-list( @@ -54,7 +54,7 @@ template(name="addCardForm") .add-controls.clearfix button.primary.confirm(type="submit") {{_ 'add'}} - a.fa.fa-times-thin.js-close-inlined-form + a.js-close-inlined-form | ❌ .add-controls.clearfix unless currentBoard.isTemplatesBoard unless currentBoard.isTemplateBoard diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 075b6282d..5c686b5ff 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -10,9 +10,6 @@ template(name="listHeader") a.list-header-left-icon.fa.fa-angle-left.js-unselect-list else if collapsed - a.js-collapse(title="{{_ 'uncollapse'}}") - i.fa.fa-arrow-left.list-header-uncollapse-left - i.fa.fa-arrow-right.list-header-uncollapse-right if showCardsCountForList cards.length br span.cardCount {{cardsCount}} @@ -29,6 +26,10 @@ template(name="listHeader") if showCardsCountForList cards.length span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}} else + if collapsed + a.js-collapse(title="{{_ 'uncollapse'}}") + | ⬅️ + | ➡️ div(class="{{#if collapsed}}list-rotated{{/if}}") h2.list-header-name( title="{{ moment modifiedAt 'LLL' }}" @@ -45,32 +46,32 @@ template(name="listHeader") if isMiniScreen if currentList if isWatching - i.list-header-watch-icon.fa.fa-eye + i.list-header-watch-icon | 👁️ div.list-header-menu unless currentUser.isCommentOnly if canSeeAddCard - a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") - a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") + a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") | ➕ + a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ☰ else - a.list-header-menu-icon.fa.fa-angle-right.js-select-list - a.list-header-handle.handle.fa.fa-arrows.js-list-handle + a.list-header-menu-icon.js-select-list | ▶️ + a.list-header-handle.handle.js-list-handle | ↔️ else if currentUser.isBoardMember if isWatching - i.list-header-watch-icon.fa.fa-eye + i.list-header-watch-icon | 👁️ unless collapsed div.list-header-menu unless currentUser.isCommentOnly //if isBoardAdmin // a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}") if canSeeAddCard - a.js-add-card.fa.fa-plus.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") + a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") | ➕ a.js-collapse(title="{{_ 'collapse'}}") - i.fa.fa-arrow-right.list-header-collapse-right - i.fa.fa-arrow-left.list-header-collapse-left - a.fa.fa-navicon.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") + | ⬅️ + | ➡️ + a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") | ☰ if currentUser.isBoardAdmin if isTouchScreenOrShowDesktopDragHandles - a.list-header-handle.handle.fa.fa-arrows.js-list-handle + a.list-header-handle.handle.js-list-handle | ↔️ template(name="editListTitleForm") .list-composer diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 96d4f818e..c9b7625eb 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -7,14 +7,14 @@ template(name="sidebar") .sidebar-actions .sidebar-shortcuts a.sidebar-btn.js-shortcuts(title="{{_ 'keyboard-shortcuts' }}") - i.fa.fa-keyboard-o + | ⌨️ span {{_ 'keyboard-shortcuts' }} a.sidebar-btn.js-keyboard-shortcuts-toggle( title="{{#if isKeyboardShortcuts}}{{_ 'keyboard-shortcuts-enabled'}}{{else}}{{_ 'keyboard-shortcuts-disabled'}}{{/if}}") - i.fa(class="fa-solid fa-{{#if isKeyboardShortcuts}}check-square-o{{else}}ban{{/if}}") + | {{#if isKeyboardShortcuts}}✅{{else}}🚫{{/if}} if isAccessibilityEnabled a.sidebar-accessibility - i.fa.fa-universal-access + | ♿ span {{_ 'accessibility'}} a.sidebar-xmark.js-close-sidebar ✕ .sidebar-content.js-board-sidebar-content @@ -22,7 +22,7 @@ template(name="sidebar") // i.fa.fa-navicon unless isDefaultView h2 - a.fa.fa-chevron-left.js-back-home + a.js-back-home | ⬅️ = getViewTitle if isOpen +Template.dynamic(template=getViewTemplate) @@ -51,7 +51,7 @@ template(name='homeSidebar') hr unless currentUser.isNoComments h3.activity-title - i.fa.fa-comments-o + | 💬 | {{_ 'activities'}} .material-toggle-switch(title="{{_ 'show-activities'}}") @@ -67,11 +67,11 @@ template(name="membersWidget") unless currentUser.isWorker h3 a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}") - i.board-header-btn-icon.fa.fa-cog + | ⚙️ | {{_ 'boardMenuPopup-title'}} hr h3 - i.fa.fa-users + | 👥 | {{_ 'members'}} +basicTabs(tabs=tabs) +tabContent(slug="people") @@ -83,15 +83,15 @@ template(name="membersWidget") if isSandstorm if currentUser.isBoardMember a.member.add-member.sandstorm-powerbox-request-identity(title="{{_ 'add-members'}}") - i.fa.fa-plus + | ➕ else if currentUser.isBoardAdmin a.member.add-member.js-manage-board-members(title="{{_ 'add-members'}}") - i.fa.fa-plus + | ➕ .clearfix if isInvited hr p - i.fa.fa-exclamation-circle + | ⚠️ | {{_ 'just-invited'}} button.js-member-invite-accept.primary {{_ 'accept'}} button.js-member-invite-decline {{_ 'decline'}} @@ -127,7 +127,7 @@ template(name="boardOrgGeneral") th if currentUser.isBoardAdmin a.member.orgOrTeamMember.add-member.js-manage-board-addOrg(title="{{_ 'add-members'}}") - i.addTeamFaPlus.fa.fa-plus + | ➕ .divaddfaplusminus | {{_ 'add'}} each org in currentBoard.activeOrgs @@ -148,7 +148,7 @@ template(name="boardTeamGeneral") th if currentUser.isBoardAdmin a.member.orgOrTeamMember.add-member.js-manage-board-addTeam(title="{{_ 'add-members'}}") - i.addTeamFaPlus.fa.fa-plus + | ➕ .divaddfaplusminus | {{_ 'add'}} each currentBoard.activeTeams @@ -161,7 +161,7 @@ template(name="boardChangeColorPopup") span.background-box(class="board-color-{{this}}") span {{this}} if isSelected - i.fa.fa-check + | ✅ template(name="boardChangeBackgroundImagePopup") form @@ -423,7 +423,7 @@ template(name="chooseBoardSource") template(name="archiveBoardPopup") p {{_ 'close-board-pop'}} button.js-confirm.negate.full(type="submit") - i.fa.fa-archive + | 📦 | {{_ 'archive'}} template(name="outgoingWebhooksPopup") @@ -459,25 +459,25 @@ template(name="boardMenuPopup") if currentUser.isBoardAdmin li a.js-open-rules-view(title="{{_ 'rules'}}") - i.fa.fa-magic + | ✨ | {{_ 'rules'}} if currentUser.isBoardAdmin li a.js-custom-fields - i.fa.fa-list-alt + | 📝 | {{_ 'custom-fields'}} li a.js-open-archives - i.fa.fa-archive + | 📦 | {{_ 'archived-items'}} if currentUser.isBoardAdmin li a.js-change-board-color - i.fa.fa-paint-brush + | 🎨 | {{_ 'board-change-color'}} li a.js-change-background-image - i.fa.fa-picture-o + | 🖼️ | {{_ 'board-change-background-image'}} //Bug Board icons random dance https://github.com/wekan/wekan/issues/4214 //if currentUser.isBoardAdmin @@ -492,20 +492,20 @@ template(name="boardMenuPopup") if withApi li a.js-export-board - i.fa.fa-share-alt + | 📤 | {{_ 'export-board'}} if currentUser.isBoardAdmin li a.js-outgoing-webhooks - i.fa.fa-globe + | 🌐 | {{_ 'outgoing-webhooks'}} li a.js-card-settings - i.fa.fa-id-card-o + | 🃏 | {{_ 'card-settings'}} li a.js-subtask-settings - i.fa.fa-sitemap + | 🌐 | {{_ 'subtask-settings'}} unless currentBoard.isTemplatesBoard if currentUser.isBoardAdmin @@ -513,41 +513,40 @@ template(name="boardMenuPopup") ul.pop-over-list li a.js-archive-board - i.fa.fa-arrow-right - i.fa.fa-archive + | ➡️📦 | {{_ 'archive-board'}} template(name="exportBoard") ul.pop-over-list li a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}") - i.fa.fa-share-alt + | 📤 | {{_ 'export-board-json'}} li a(href="{{exportUrlExcel}}", download="{{exportFilenameExcel}}") - i.fa.fa-share-alt + | 📤 | {{_ 'export-board-excel'}} li a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}") - i.fa.fa-share-alt + | 📤 | {{_ 'export-board-csv'}} , li a(href="{{exportScsvUrl}}", download="{{exportCsvFilename}}") - i.fa.fa-share-alt + | 📤 | {{_ 'export-board-csv'}} ; li a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}") - i.fa.fa-share-alt + | 📤 | {{_ 'export-board-tsv'}} li a.html-export-board - i.fa.fa-archive + | 📦 | {{_ 'export-board-html'}} template(name="labelsWidget") .board-widget.board-widget-labels h3 - i.fa.fa-tags + | 🏷️ | {{_ 'labels'}} .board-widget-content each currentBoard.labels @@ -558,7 +557,7 @@ template(name="labelsWidget") = name if currentUser.isBoardAdmin a.card-label.add-label.js-add-label(title="{{_ 'label-create'}}") - i.fa.fa-plus + | ➕ template(name="memberPopup") .board-member-menu @@ -570,7 +569,7 @@ template(name="memberPopup") p.quiet @#{user.username} if isInvited p - i.fa.fa-exclamation-circle + | ⚠️ | {{_ 'not-accepted-yet'}} ul.pop-over-list @@ -665,31 +664,31 @@ template(name="changePermissionsPopup") a(class="{{#if isLastAdmin}}disabled{{else}}js-set-admin{{/if}}") | {{_ 'admin'}} if isAdmin - i.fa.fa-check + | ✅ span.sub-name {{_ 'admin-desc'}} li a(class="{{#if isLastAdmin}}disabled{{else}}js-set-normal{{/if}}") | {{_ 'normal'}} if isNormal - i.fa.fa-check + | ✅ span.sub-name {{_ 'normal-desc'}} li a(class="{{#if isLastAdmin}}disabled{{else}}js-set-no-comments{{/if}}") | {{_ 'no-comments'}} if isNoComments - i.fa.fa-check + | ✅ span.sub-name {{_ 'no-comments-desc'}} li a(class="{{#if isLastAdmin}}disabled{{else}}js-set-comment-only{{/if}}") | {{_ 'comment-only'}} if isCommentOnly - i.fa.fa-check + | ✅ span.sub-name {{_ 'comment-only-desc'}} li a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}") | {{_ 'worker'}} if isWorker - i.fa.fa-check + | ✅ span.sub-name {{_ 'worker-desc'}} if isLastAdmin hr diff --git a/client/components/swimlanes/swimlaneHeader.jade b/client/components/swimlanes/swimlaneHeader.jade index fac00e23b..109076e45 100644 --- a/client/components/swimlanes/swimlaneHeader.jade +++ b/client/components/swimlanes/swimlaneHeader.jade @@ -24,10 +24,10 @@ template(name="swimlaneFixedHeader") | {{isTitleDefault title}} .swimlane-header-menu unless currentUser.isCommentOnly - a.js-open-add-swimlane-menu.swimlane-header-plus-icon - | ➕(title="{{_ 'add-swimlane'}}") - a.js-open-swimlane-menu - | ☰(title="{{_ 'swimlaneActionPopup-title'}}") + a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}") + | ➕ + a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}") + | ☰ //// TODO: Collapse Swimlane: make button working, etc. //unless collapsed // a.js-collapse-swimlane(title="{{_ 'collapse'}}") @@ -99,7 +99,7 @@ template(name="setSwimlaneColorPopup") each colors span.card-label.palette-color.js-palette-color(class="card-details-{{color}}") if(isSelected color) - i.fa.fa-check + | ✅ // Buttons aligned left too .flush-left button.primary.confirm.js-submit(style="margin-left:0") {{_ 'save'}} diff --git a/client/components/swimlanes/swimlanes.css b/client/components/swimlanes/swimlanes.css index 7a66f95d7..4c20cb0f4 100644 --- a/client/components/swimlanes/swimlanes.css +++ b/client/components/swimlanes/swimlanes.css @@ -8,6 +8,7 @@ flex-direction: row; overflow: auto; max-height: 100%; + position: relative; } .swimlane-header-menu .swimlane-header-collapse-down { font-size: 50%; @@ -234,3 +235,106 @@ background: #4b0082 !important; color: #fff !important; } + +/* Swimlane resize handle */ +.swimlane-resize-handle { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 8px; + background: transparent; + cursor: row-resize; + z-index: 20; + border-top: 2px solid transparent; + transition: all 0.2s ease; + border-radius: 2px; + /* Ensure the handle is clickable */ + pointer-events: auto; +} + +/* Show resize handle only on hover */ +.swimlane:hover .swimlane-resize-handle { + background: rgba(0, 0, 0, 0.1); + border-top-color: rgba(0, 0, 0, 0.2); +} + +/* Add a subtle resize indicator line at the bottom of swimlane on hover */ +.swimlane:hover .swimlane-resize-handle::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: rgba(0, 123, 255, 0.3); + z-index: 21; + transition: all 0.2s ease; + border-radius: 1px; +} + +/* Make the indicator line more prominent when hovering over the resize handle */ +.swimlane-resize-handle:hover::after { + background: rgba(0, 123, 255, 0.6) !important; + height: 3px !important; + box-shadow: 0 0 4px rgba(0, 123, 255, 0.2); +} + +.swimlane-resize-handle:hover { + background: rgba(0, 123, 255, 0.4) !important; + border-top-color: #0079bf !important; + box-shadow: 0 0 4px rgba(0, 123, 255, 0.3); +} + +.swimlane-resize-handle:active { + background: rgba(0, 123, 255, 0.6) !important; + border-top-color: #0079bf !important; + box-shadow: 0 0 6px rgba(0, 123, 255, 0.4); +} + +/* Add a subtle indicator line */ +.swimlane-resize-handle::before { + content: ''; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 20px; + height: 2px; + background: rgba(0, 123, 255, 0.6); + border-radius: 1px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.swimlane-resize-handle:hover::before { + opacity: 1; +} + +/* Visual feedback during resize */ +.swimlane.swimlane-resizing { + transition: none !important; + box-shadow: 0 0 10px rgba(0, 123, 255, 0.3); + /* Ensure the swimlane maintains its new height during resize */ + flex: none !important; + flex-basis: auto !important; + flex-grow: 0 !important; + flex-shrink: 0 !important; + /* Override any conflicting layout properties */ + display: flex !important; + position: relative !important; + /* Force height to be respected */ + height: var(--swimlane-height, auto) !important; + min-height: var(--swimlane-height, auto) !important; + max-height: var(--swimlane-height, auto) !important; + /* Ensure the height is applied immediately */ + overflow: visible !important; +} + +body.swimlane-resizing-active { + cursor: row-resize !important; +} + +body.swimlane-resizing-active * { + cursor: row-resize !important; +} diff --git a/client/components/swimlanes/swimlanes.jade b/client/components/swimlanes/swimlanes.jade index 0afe02dbe..b2d83ceac 100644 --- a/client/components/swimlanes/swimlanes.jade +++ b/client/components/swimlanes/swimlanes.jade @@ -4,6 +4,7 @@ template(name="swimlane") unless collapseSwimlane .swimlane.js-lists.js-swimlane.dragscroll(id="swimlane-{{_id}}" style="height:{{swimlaneHeight}};") + .swimlane-resize-handle.js-swimlane-resize-handle.nodragscroll if isMiniScreen if currentListIsInThisSwimlane _id +list(currentList) @@ -67,7 +68,7 @@ template(name="addListForm") a.js-list-template {{_ 'template'}} else a.open-list-composer.js-open-inlined-form(title="{{_ 'add-list'}}") - i.fa.fa-plus + | ➕ template(name="moveSwimlanePopup") unless currentUser.isWorker diff --git a/client/components/swimlanes/swimlanes.js b/client/components/swimlanes/swimlanes.js index 82e8b96e4..dc149c48c 100644 --- a/client/components/swimlanes/swimlanes.js +++ b/client/components/swimlanes/swimlanes.js @@ -247,10 +247,21 @@ BlazeComponent.extendComponent({ Utils.isTouchScreenOrShowDesktopDragHandles() ? ['.js-list-handle', '.js-swimlane-header-handle'] : ['.js-list-header'], - ); + ).concat([ + '.js-list-resize-handle', + '.js-swimlane-resize-handle' + ]); + const isResizeHandle = $(evt.target).closest('.js-list-resize-handle, .js-swimlane-resize-handle').length > 0; + const isInNoDragArea = $(evt.target).closest(noDragInside.join(',')).length > 0; + + if (isResizeHandle) { + console.log('Board drag prevented - resize handle clicked'); + return; + } + if ( - $(evt.target).closest(noDragInside.join(',')).length === 0 && + !isInNoDragArea && this.$('.swimlane').prop('clientHeight') > evt.offsetY ) { this._isDragging = true; @@ -283,9 +294,150 @@ BlazeComponent.extendComponent({ swimlaneHeight() { const user = ReactiveCache.getCurrentUser(); const swimlane = Template.currentData(); - const height = user.getSwimlaneHeight(swimlane.boardId, swimlane._id); + const height = user.getSwimlaneHeightFromStorage(swimlane.boardId, swimlane._id); return height == -1 ? "auto" : (height + 5 + "px"); }, + + onRendered() { + // Initialize swimlane resize functionality immediately + this.initializeSwimlaneResize(); + }, + + initializeSwimlaneResize() { + // Check if we're still in a valid template context + if (!Template.currentData()) { + console.warn('No current template data available for swimlane resize initialization'); + return; + } + + const swimlane = Template.currentData(); + const $swimlane = $(`#swimlane-${swimlane._id}`); + const $resizeHandle = $swimlane.find('.js-swimlane-resize-handle'); + + // Check if elements exist + if (!$swimlane.length || !$resizeHandle.length) { + console.warn('Swimlane or resize handle not found, retrying in 100ms'); + Meteor.setTimeout(() => { + if (!this.isDestroyed) { + this.initializeSwimlaneResize(); + } + }, 100); + return; + } + + + if ($resizeHandle.length === 0) { + return; + } + + let isResizing = false; + let startY = 0; + let startHeight = 0; + const minHeight = 100; + const maxHeight = 2000; + + const startResize = (e) => { + isResizing = true; + startY = e.pageY || e.originalEvent.touches[0].pageY; + startHeight = parseInt($swimlane.css('height')) || 300; + + + $swimlane.addClass('swimlane-resizing'); + $('body').addClass('swimlane-resizing-active'); + $('body').css('user-select', 'none'); + + + e.preventDefault(); + e.stopPropagation(); + }; + + const doResize = (e) => { + if (!isResizing) { + return; + } + + const currentY = e.pageY || e.originalEvent.touches[0].pageY; + const deltaY = currentY - startY; + const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY)); + + + // Apply the new height immediately for real-time feedback + $swimlane[0].style.setProperty('--swimlane-height', `${newHeight}px`); + $swimlane[0].style.setProperty('height', `${newHeight}px`); + $swimlane[0].style.setProperty('min-height', `${newHeight}px`); + $swimlane[0].style.setProperty('max-height', `${newHeight}px`); + $swimlane[0].style.setProperty('flex', 'none'); + $swimlane[0].style.setProperty('flex-basis', 'auto'); + $swimlane[0].style.setProperty('flex-grow', '0'); + $swimlane[0].style.setProperty('flex-shrink', '0'); + + + e.preventDefault(); + e.stopPropagation(); + }; + + const stopResize = (e) => { + if (!isResizing) return; + + isResizing = false; + + // Calculate final height + const currentY = e.pageY || e.originalEvent.touches[0].pageY; + const deltaY = currentY - startY; + const finalHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY)); + + // Ensure the final height is applied + $swimlane[0].style.setProperty('--swimlane-height', `${finalHeight}px`); + $swimlane[0].style.setProperty('height', `${finalHeight}px`); + $swimlane[0].style.setProperty('min-height', `${finalHeight}px`); + $swimlane[0].style.setProperty('max-height', `${finalHeight}px`); + $swimlane[0].style.setProperty('flex', 'none'); + $swimlane[0].style.setProperty('flex-basis', 'auto'); + $swimlane[0].style.setProperty('flex-grow', '0'); + $swimlane[0].style.setProperty('flex-shrink', '0'); + + // Remove visual feedback but keep the height + $swimlane.removeClass('swimlane-resizing'); + $('body').removeClass('swimlane-resizing-active'); + $('body').css('user-select', ''); + + // Save the new height using the existing system + const boardId = swimlane.boardId; + const swimlaneId = swimlane._id; + + if (process.env.DEBUG === 'true') { + } + + // Use the new storage method that handles both logged-in and non-logged-in users + Meteor.call('applySwimlaneHeightToStorage', boardId, swimlaneId, finalHeight, (error, result) => { + if (error) { + console.error('Error saving swimlane height:', error); + } else { + if (process.env.DEBUG === 'true') { + } + } + }); + + e.preventDefault(); + }; + + // Mouse events + $resizeHandle.on('mousedown', startResize); + $(document).on('mousemove', doResize); + $(document).on('mouseup', stopResize); + + // Touch events for mobile + $resizeHandle.on('touchstart', startResize, { passive: false }); + $(document).on('touchmove', doResize, { passive: false }); + $(document).on('touchend', stopResize, { passive: false }); + + + // Prevent dragscroll interference + $resizeHandle.on('mousedown', (e) => { + e.stopPropagation(); + }); + + }, }).register('swimlane'); BlazeComponent.extendComponent({ diff --git a/client/components/users/userHeader.jade b/client/components/users/userHeader.jade index b0c6b120d..41a2ed70b 100644 --- a/client/components/users/userHeader.jade +++ b/client/components/users/userHeader.jade @@ -173,7 +173,7 @@ template(name="changeLanguagePopup") a.js-set-language = name if isCurrentLanguage - i.fa.fa-check + | ✅ template(name="changeSettingsPopup") ul.pop-over-list @@ -186,7 +186,7 @@ template(name="changeSettingsPopup") unless currentUser.isWorker li label.bold.clear - i.fa.fa-sort-numeric-asc + | 🔢 | {{_ 'show-cards-minimum-count'}} input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="-1") label.bold.clear diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js index f42dfed94..967b83059 100644 --- a/client/config/blazeHelpers.js +++ b/client/config/blazeHelpers.js @@ -65,8 +65,8 @@ Blaze.registerHelper('isTouchScreenOrShowDesktopDragHandles', () => Blaze.registerHelper('moment', (...args) => { args.pop(); // hash - const [date, format] = args; - return format(new Date(date), format ?? 'LLLL'); + const [date, formatStr] = args; + return format(new Date(date), formatStr ?? 'LLLL'); }); Blaze.registerHelper('canModifyCard', () => diff --git a/models/users.js b/models/users.js index 329a42fab..081df38d6 100644 --- a/models/users.js +++ b/models/users.js @@ -875,6 +875,52 @@ Users.helpers({ } }, + getSwimlaneHeightFromStorage(boardId, swimlaneId) { + // For logged-in users, get from profile + if (this._id) { + return this.getSwimlaneHeight(boardId, swimlaneId); + } + + // For non-logged-in users, get from localStorage + try { + const stored = localStorage.getItem('wekan-swimlane-heights'); + if (stored) { + const heights = JSON.parse(stored); + if (heights[boardId] && heights[boardId][swimlaneId]) { + return heights[boardId][swimlaneId]; + } + } + } catch (e) { + console.warn('Error reading swimlane heights from localStorage:', e); + } + + return -1; + }, + + setSwimlaneHeightToStorage(boardId, swimlaneId, height) { + // For logged-in users, save to profile + 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) { + console.warn('Error saving swimlane height to localStorage:', e); + return false; + } + }, + /** returns all confirmed move and copy dialog field values *