Resolve merge conflicts by accepting PR #6131 changes

Co-authored-by: xet7 <15545+xet7@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-02-07 16:30:08 +00:00
parent dc0b68ee80
commit 97dd5d2064
257 changed files with 9483 additions and 14103 deletions

View file

@ -1,16 +1,22 @@
.activity-title {
margin: 0 0.7vw 1vh;
display: flex;
gap: 0.5lh;
justify-content: space-between;
}
.reactions-popup {
display: flex;
gap: 1ch;
flex-wrap: wrap;
margin: 0.5lh 0.5ch;
max-width: 80vw;
}
.reactions-popup .add-comment-reaction {
display: inline-block;
cursor: pointer;
border-radius: 0.7vw;
font-size: clamp(18px, 4vw, 22px);
text-align: center;
line-height: 1.3;
width: 5vw;
font-size: 1.2em;
}
.reactions-popup .add-comment-reaction:hover {
background-color: #b0c4de;
@ -18,20 +24,20 @@
.activities {
clear: both;
}
.activity {
display: flex;
}
.activities .activity {
margin: 0.1vh 0;
padding: 0.8vh 0;
display: flex;
}
.activities .activity .member {
width: 4vw;
height: 4vw;
font-size: 0.8em;
}
.activities .activity .activity-member {
font-weight: 700;
}
.activities .activity .activity-desc {
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
flex: 1;
align-self: center;

View file

@ -275,7 +275,7 @@ Template.commentReactions.events({
cardComment.toggleReaction(codepoint);
}
},
'click .open-comment-reaction-popup': Popup.open('addReaction'),
'click .open-comment-reaction-popup': Popup.open('addReaction', {showHeader: false})
})
Template.addReactionPopup.events({
@ -306,6 +306,11 @@ Template.addReactionPopup.helpers({
'&#128522;',
'&#129300;',
'&#128532;'];
},
hasUserReacted(codepoint) {
const commentId = Template.instance().data.commentId;
const cardComment = ReactiveCache.getCardComment(commentId);
return cardComment.hasUserReacted(codepoint);
}
})

View file

@ -1,12 +1,12 @@
.new-comment {
position: relative;
margin: 0 0 20px 38px;
display: flex;
align-items: center;
justify-content: stretch;
gap: 1ch;
}
.new-comment .member {
opacity: 0.7;
position: absolute;
top: 1px;
left: -38px;
}
.new-comment.is-open .member {
opacity: 1;
@ -14,34 +14,44 @@
.new-comment.is-open .helper {
display: inline-block;
}
.new-comment.is-open textarea {
min-height: 100px;
color: #4d4d4d;
cursor: auto;
overflow: hidden;
word-wrap: break-word;
.new-comment, .comment {
.is-open textarea {
min-height: 100px;
color: #4d4d4d;
cursor: auto;
overflow-wrap: break-word;
}
textarea {
grid-area: editor;
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
min-height: 3lh;
&:hover, &.is-open {
cursor: auto;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.33);
border: 0;
cursor: pointer;
}
}
}
.new-comment .too-long {
margin-top: 8px;
}
.new-comment textarea {
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
height: 36px;
margin: 4px 4px 6px 0;
padding: 9px 11px;
width: 100%;
}
.new-comment textarea:hover,
.new-comment textarea:is-open {
background-color: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.33);
border: 0;
cursor: pointer;
}
.new-comment textarea:is-open {
cursor: auto;
.js-new-comment-form, .js-edit-comment {
display: grid !important;
grid-template-areas:
"editor editor editor editor"
"main-controls main-controls link-controls editor-controls";
grid-auto-columns: 1fr;
grid-auto-rows: min-content;
align-items: center;
flex: 1;
gap: 0.3lh;
}
.comment-item {
background-color: #fff;
@ -65,31 +75,30 @@
}
.comments {
clear: both;
display: flex;
flex-direction: column;
gap: 1lh;
padding-top: 1lh;
}
.comments .comment {
margin: 0.5px 0;
padding: 6px 0;
display: flex;
gap: 1ch;
}
.comments .comment .member {
width: 32px;
height: 32px;
}
.comments .comment .comment-member {
font-weight: 700;
}
.comments .comment .comment-desc {
word-wrap: break-word;
overflow: hidden;
overflow-wrap: break-word;
flex: 1;
align-self: center;
margin: 0;
margin-left: 3px;
overflow: hidden;
word-break: break-word;
display: flex;
flex-direction: column;
gap: 0.3lh;
}
.comments .comment .comment-desc .comment-text {
display: block;
display: flex;
border-radius: 3px;
background: #fff;
text-decoration: none;
@ -101,6 +110,7 @@
display: flex;
margin-top: 5px;
gap: 5px;
align-items: center;
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup {
display: flex;
@ -110,7 +120,6 @@
}
.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
display: inline-block;
font-size: clamp(14px, 2vw, 18px);
font-weight: 500;
line-height: 1;
margin-left: 4px;
@ -128,10 +137,14 @@
.comments .comment .comment-desc .reactions .reaction:hover {
background-color: #b0c4de;
}
.comments .comment .comment-desc .reactions .reaction .reaction-count {
font-size: 12px;
}
.comments .comment .comment-desc .comment-meta {
font-size: 0.8em;
color: #999;
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: 1ch;
align-items: center;
/* #FIXME maybe put date outside of comment body ?*/
margin-inline-start: -10vw;
}

View file

@ -64,5 +64,5 @@ template(name="commentReactions")
template(name="addReactionPopup")
.reactions-popup
each codepoint in codepoints
span.add-comment-reaction(data-codepoint="#{codepoint}") !{codepoint}
unless (hasUserReacted codepoint)
span.add-comment-reaction(data-codepoint="#{codepoint}") !{codepoint}

View file

@ -18,7 +18,7 @@
.board-conversion-modal {
background: white;
border-radius: 8px;
border-radius: 0.8ch;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
@ -47,7 +47,7 @@
.board-conversion-header h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 20px;
font-weight: 500;
}
@ -59,7 +59,7 @@
.board-conversion-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.board-conversion-content {
@ -74,7 +74,7 @@
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
border-radius: 0.4ch;
overflow: hidden;
margin-bottom: 8px;
}
@ -82,7 +82,7 @@
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #2196F3, #21CBF3);
border-radius: 4px;
border-radius: 0.4ch;
transition: width 0.3s ease;
position: relative;
}
@ -116,14 +116,14 @@
text-align: center;
font-weight: 600;
color: #2196F3;
font-size: 16px;
}
.conversion-status {
text-align: center;
margin-bottom: 16px;
color: #333;
font-size: 16px;
}
.conversion-status i {
@ -134,10 +134,10 @@
.conversion-time {
text-align: center;
color: #666;
font-size: 14px;
background-color: #f5f5f5;
padding: 8px 12px;
border-radius: 4px;
border-radius: 0.4ch;
margin-bottom: 16px;
}
@ -155,7 +155,7 @@
.conversion-info {
text-align: center;
color: #666;
font-size: 13px;
line-height: 1.4;
}
@ -170,15 +170,15 @@
width: 95%;
margin: 20px;
}
.board-conversion-header,
.board-conversion-content,
.board-conversion-footer {
padding-left: 16px;
padding-right: 16px;
}
.board-conversion-header h3 {
font-size: 18px;
}
}

View file

@ -1,7 +1,7 @@
template(name="archivedBoards")
h2
span(title="{{_ 'archived-boards'}}")
i.fa.fa-archive
i.fa.fa-archive
| {{_ 'archived-boards'}}
ul.archived-lists

View file

@ -1,43 +1,25 @@
.swim-flex {
display: flex;
flex: 1;
flex-direction: column;
align-items: stretch;
padding-bottom: 40vw;
}
.board-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow-x: hidden;
overflow-y: hidden;
width: 100%;
min-width: 100%;
display: flex;
flex: 1;
overflow: auto;
}
/* When zoom is 50% or lower, ensure full width like content */
.board-wrapper[style*="transform: scale(0.5)"] {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
.board-wrapper[style*="transform: scale(0.4)"] {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
.board-wrapper[style*="transform: scale(0.3)"] {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
}
.board-wrapper .board-canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
transition: margin 0.1s;
overflow-y: auto;
width: 100%;
min-width: 100%;
overflow-x: hidden;
display: flex;
/* don't stretch vertically if not needed (e.g collapsed) */
align-self: start;
flex: 1;
}
/* Ensure horizontal scrollbar is visible for high zoom levels */
@ -97,172 +79,12 @@
position: relative;
}
#content[style*="overflow-x: auto"]::-webkit-scrollbar {
height: 12px;
width: 12px;
/* Force vertical scrollbar to always be visible */
#content[style*="overflow-y: auto"] {
overflow-y: scroll !important;
}
/* Force vertical scrollbar to always be visible */
#content[style*="overflow-y: auto"] {
overflow-y: scroll !important;
}
/* Mobile - make all text 2x bigger inside #content by default (icons stay same size) */
@media screen and (max-width: 800px),
screen and (max-device-width: 800px),
screen and (-webkit-min-device-pixel-ratio: 2) and (max-width: 800px),
screen and (max-width: 800px) and (orientation: portrait),
screen and (max-width: 800px) and (orientation: landscape) {
#content {
font-size: 2em !important; /* 2x bigger base font size for content area */
}
/* Make all text elements 2x bigger */
#content h1, #content h2, #content h3, #content h4, #content h5, #content h6,
#content p, #content span, #content div, #content a, #content button,
#content .minicard, #content .list-header-name, #content .board-header-btn,
#content .card-title, #content .card-details, #content .card-description,
#content .swimlane-header, #content .list-title, #content .card-text,
#content .member, #content .member-name, #content .member-initials,
#content .checklist-item, #content .checklist-title, #content .comment,
#content .activity, #content .activity-text, #content .activity-time,
#content .board-title, #content .board-description, #content .list-name,
#content .card-text, #content .card-title, #content .card-description,
#content .swimlane-title, #content .swimlane-description,
#content .board-header-title, #content .board-header-description,
#content .card-detail-title, #content .card-detail-description,
#content .list-header-title, #content .list-header-description,
#content .swimlane-header-title, #content .swimlane-header-description,
#content .minicard-title, #content .minicard-description,
#content .card-comment, #content .card-comment-text,
#content .checklist-item-text, #content .checklist-item-title,
#content .activity-item, #content .activity-item-text,
#content .board-member, #content .board-member-name,
#content .team-member, #content .team-member-name,
#content .org-member, #content .org-member-name,
#content .template-member, #content .template-member-name,
#content .user-name, #content .user-email, #content .user-role,
#content .setting-title, #content .setting-description,
#content .popup-title, #content .popup-description,
#content .modal-title, #content .modal-description,
#content .notification-title, #content .notification-text,
#content .announcement-title, #content .announcement-text,
#content .offline-warning-title, #content .offline-warning-text,
#content .error-title, #content .error-text,
#content .success-title, #content .success-text,
#content .info-title, #content .info-text,
#content .warning-title, #content .warning-text {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* Keep icons the same size (don't scale them) */
#content .fa, #content .icon, #content i {
font-size: 1em !important; /* Keep original icon size */
}
/* Reset specific icon sizes to prevent double scaling */
#content .fa-home, #content .fa-bars, #content .fa-search,
#content .fa-bell, #content .fa-user, #content .fa-cog,
#content .fa-plus, #content .fa-minus, #content .fa-edit,
#content .fa-trash, #content .fa-save, #content .fa-cancel,
#content .fa-arrow-left, #content .fa-arrow-right,
#content .fa-arrow-up, #content .fa-arrow-down,
#content .fa-check, #content .fa-times, #content .fa-close,
#content .fa-star, #content .fa-heart, #content .fa-thumbs-up,
#content .fa-thumbs-down, #content .fa-comment, #content .fa-reply,
#content .fa-share, #content .fa-download, #content .fa-upload,
#content .fa-copy, #content .fa-paste, #content .fa-cut,
#content .fa-undo, #content .fa-redo, #content .fa-refresh,
#content .fa-sync, #content .fa-spinner, #content .fa-loading,
#content .fa-info, #content .fa-question, #content .fa-exclamation,
#content .fa-warning, #content .fa-error, #content .fa-success,
#content .fa-check-circle, #content .fa-times-circle,
#content .fa-exclamation-circle, #content .fa-question-circle,
#content .fa-info-circle, #content .fa-warning-circle,
#content .fa-error-circle, #content .fa-success-circle {
font-size: 1em !important; /* Keep original icon size */
}
}
/* Fallback for iPhone devices using JavaScript detection */
.iphone-device #content {
font-size: 2em !important; /* 2x bigger base font size for content area */
}
.iphone-device #content h1, .iphone-device #content h2, .iphone-device #content h3, .iphone-device #content h4, .iphone-device #content h5, .iphone-device #content h6,
.iphone-device #content p, .iphone-device #content span, .iphone-device #content div, .iphone-device #content a, .iphone-device #content button,
.iphone-device #content .minicard, .iphone-device #content .list-header-name, .iphone-device #content .board-header-btn,
.iphone-device #content .card-title, .iphone-device #content .card-details, .iphone-device #content .card-description,
.iphone-device #content .swimlane-header, .iphone-device #content .list-title, .iphone-device #content .card-text,
.iphone-device #content .member, .iphone-device #content .member-name, .iphone-device #content .member-initials,
.iphone-device #content .checklist-item, .iphone-device #content .checklist-title, .iphone-device #content .comment,
.iphone-device #content .activity, .iphone-device #content .activity-text, .iphone-device #content .activity-time,
.iphone-device #content .board-title, .iphone-device #content .board-description, .iphone-device #content .list-name,
.iphone-device #content .card-text, .iphone-device #content .card-title, .iphone-device #content .card-description,
.iphone-device #content .swimlane-title, .iphone-device #content .swimlane-description,
.iphone-device #content .board-header-title, .iphone-device #content .board-header-description,
.iphone-device #content .card-detail-title, .iphone-device #content .card-detail-description,
.iphone-device #content .list-header-title, .iphone-device #content .list-header-description,
.iphone-device #content .swimlane-header-title, .iphone-device #content .swimlane-header-description,
.iphone-device #content .minicard-title, .iphone-device #content .minicard-description,
.iphone-device #content .card-comment, .iphone-device #content .card-comment-text,
.iphone-device #content .checklist-item-text, .iphone-device #content .checklist-item-title,
.iphone-device #content .activity-item, .iphone-device #content .activity-item-text,
.iphone-device #content .board-member, .iphone-device #content .board-member-name,
.iphone-device #content .team-member, .iphone-device #content .team-member-name,
.iphone-device #content .org-member, .iphone-device #content .org-member-name,
.iphone-device #content .template-member, .iphone-device #content .template-member-name,
.iphone-device #content .user-name, .iphone-device #content .user-email, .iphone-device #content .user-role,
.iphone-device #content .setting-title, .iphone-device #content .setting-description,
.iphone-device #content .popup-title, .iphone-device #content .popup-description,
.iphone-device #content .modal-title, .iphone-device #content .modal-description,
.iphone-device #content .notification-title, .iphone-device #content .notification-text,
.iphone-device #content .announcement-title, .iphone-device #content .announcement-text,
.iphone-device #content .offline-warning-title, .iphone-device #content .offline-warning-text,
.iphone-device #content .error-title, .iphone-device #content .error-text,
.iphone-device #content .success-title, .iphone-device #content .success-text,
.iphone-device #content .info-title, .iphone-device #content .info-text,
.iphone-device #content .warning-title, .iphone-device #content .warning-text {
font-size: 1em !important; /* Use inherited 2x scaling */
}
/* Keep icons the same size for iPhone devices */
.iphone-device #content .fa, .iphone-device #content .icon, .iphone-device #content i {
font-size: 1em !important; /* Keep original icon size */
}
/* Mobile iPhone: scale card details text and icons to 2x */
body.mobile-mode.iphone-device .card-details {
font-size: 2em !important;
}
body.mobile-mode.iphone-device .card-details .fa,
body.mobile-mode.iphone-device .card-details .icon,
body.mobile-mode.iphone-device .card-details i,
body.mobile-mode.iphone-device .card-details .emoji-icon,
body.mobile-mode.iphone-device .card-details a,
body.mobile-mode.iphone-device .card-details p,
body.mobile-mode.iphone-device .card-details span,
body.mobile-mode.iphone-device .card-details div,
body.mobile-mode.iphone-device .card-details button,
body.mobile-mode.iphone-device .card-details input,
body.mobile-mode.iphone-device .card-details select,
body.mobile-mode.iphone-device .card-details textarea {
font-size: inherit !important;
}
/* Section titles slightly larger than content but not as big as card title */
body.mobile-mode.iphone-device .card-details .card-details-item-title {
font-size: 1.1em !important;
font-weight: bold;
}
/* Ensure scrollbars are positioned correctly */
#content[style*="overflow-x: auto"]::-webkit-scrollbar:vertical {
width: 12px;
}
#content[style*="overflow-x: auto"]::-webkit-scrollbar:horizontal {
height: 12px;
}
/* Force both scrollbars to always be visible for high zoom levels */
#content[style*="overflow-x: auto"][style*="overflow-y: auto"] {
@ -274,36 +96,6 @@ body.mobile-mode.iphone-device .card-details .card-details-item-title {
#content[style*="overflow-y: auto"] {
scrollbar-gutter: stable;
}
.board-wrapper .board-canvas .board-overlay {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
top: -100px;
right: -400px;
background: #000;
opacity: 0.33;
animation: fadeIn 0.2s;
z-index: 16;
}
/* Fix for mobile Safari: ensure overlay stays behind card details */
@media screen and (max-width: 800px) {
.board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
/* In desktop mode on small screens, still keep overlay behind card */
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
}
/* In mobile mode, lower the overlay z-index to stay behind card details */
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
/* iPhone in desktop mode: remove overlay to avoid blocking card */
body.desktop-mode.iphone-device .board-wrapper .board-canvas .board-overlay {
@ -320,73 +112,14 @@ body.desktop-mode .board-wrapper .board-canvas .board-overlay {
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
display: none;
}
/* Mobile view styles - applied when isMiniScreen is true (iPhone, etc.) */
.board-wrapper.mobile-view {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper.mobile-view .board-canvas {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper.mobile-view .board-canvas.mobile-view .swimlane {
border-bottom: 1px solid #ccc;
display: block !important;
flex-direction: column;
margin: 0;
padding: 0;
overflow-x: hidden !important;
overflow-y: auto;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
}
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
.board-wrapper {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper .board-canvas {
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
left: 0 !important;
right: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.board-wrapper .board-canvas .swimlane {
border-bottom: 1px solid #ccc;
display: block !important;
flex-direction: column;
margin: 0;
padding: 0;
overflow-x: hidden !important;
overflow-y: auto;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
.board-wrapper .board-canvas .swimlane {
/* this effectively prevents board
to shrink */
min-width: 100vw;
}
}
.calendar-event-green {
@ -545,7 +278,6 @@ body.desktop-mode .board-wrapper .board-canvas .board-overlay {
justify-content: center;
align-items: center;
margin: 0;
font-size: 18px;
}
.modal-footer {
display: flex;
@ -558,10 +290,6 @@ body.desktop-mode .board-wrapper .board-canvas .board-overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 5px;
right: 5px;
font-size: 25px;
cursor: pointer;
}

View file

@ -1,13 +1,9 @@
template(name="board")
if isConverting.get
+boardConversionProgress
else if isBoardReady.get
if currentBoard
if onlyShowCurrentCard
+cardDetails(currentCard)
else
+boardBody
+boardBody
else
//-- XXX We need a better error message in case the board has been archived
+message(label="board-not-found")
@ -17,32 +13,32 @@ template(name="board")
template(name="boardBody")
if notDisplayThisBoard
| {{_ 'tableVisibilityMode-allowPrivateOnly'}}
| {{_ 'tableVisibilityMode-allowPrivateOnly'}}
else
// Debug information (remove in production)
//- Debug information (remove in production)
if debugBoardState
//- Debug information (remove in production)
.debug-info(style="position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.8); color: white; padding: 10px; z-index: 9999; font-size: 12px;")
| {{_ 'board'}}: {{currentBoard.title}} | {{_ 'view'}}: {{boardView}} | {{_ 'has-swimlanes'}}: {{hasSwimlanes}} | {{_ 'swimlanes'}}: {{currentBoard.swimlanes.length}}
.board-wrapper(class=currentBoard.colorClass class="{{#if isMiniScreen}}mobile-view{{/if}}")
.board-canvas.js-swimlanes(
class="{{#if hasSwimlanes}}dragscroll{{/if}}"
class="{{#if Sidebar.isOpen}}is-sibling-sidebar-open{{/if}}"
class="{{#if MultiSelection.isActive}}is-multiselection-active{{/if}}"
class="{{#if draggingActive.get}}is-dragging-active{{/if}}"
class="{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}"
class="{{#if isMiniScreen}}mobile-view{{/if}}")
if showOverlay.get
.board-overlay
if currentBoard.isTemplatesBoard
each currentBoard.swimlanes
+swimlane(this)
else if isViewSwimlanes
if hasSwimlanes
.swim-flex
each currentBoard.swimlanes
+swimlane(this)
else
// Fallback: If no swimlanes exist, show lists instead of empty message
+listsGroup(currentBoard)
else if isViewSwimlanes
.swim-flex
if hasSwimlanes
each currentBoard.swimlanes
+swimlane(this)
else
// Fallback: If no swimlanes exist, show lists instead of empty message
+listsGroup(currentBoard)
else if isViewLists
+listsGroup(currentBoard)
else if isViewCalendar
@ -56,10 +52,6 @@ template(name="boardBody")
+swimlane(this)
else
+listsGroup(currentBoard)
//- Render multiple open cards in desktop mode
unless isMiniScreen
each openCards
+cardDetails(this cardIndex=@index)
+sidebar
template(name="calendarView")

View file

@ -27,16 +27,16 @@ BlazeComponent.extendComponent({
this.autorun(() => {
const currentBoardId = Session.get('currentBoard');
if (!currentBoardId) return;
const handle = subManager.subscribe('board', currentBoardId, false);
// Use a separate autorun for subscription ready state to avoid reactive loops
this.subscriptionReadyAutorun = Tracker.autorun(() => {
if (handle.ready()) {
if (!this._boardProcessed || this._lastProcessedBoardId !== currentBoardId) {
this._boardProcessed = true;
this._lastProcessedBoardId = currentBoardId;
// Ensure default swimlane exists (only once per board)
this.ensureDefaultSwimlane(currentBoardId);
// Check if board needs conversion
@ -67,7 +67,7 @@ BlazeComponent.extendComponent({
if (!board) return;
const swimlanes = board.swimlanes();
if (swimlanes.length === 0) {
// Check if any swimlane exists in the database to avoid race conditions
const existingSwimlanes = ReactiveCache.getSwimlanes({ boardId });
@ -105,7 +105,6 @@ BlazeComponent.extendComponent({
this.isBoardReady.set(true); // Show board even if conversion check failed
}
},
onlyShowCurrentCard() {
const isMiniScreen = Utils.isMiniScreen();
const currentCardId = Utils.getCurrentCardId(true);
@ -114,7 +113,7 @@ BlazeComponent.extendComponent({
openCards() {
// In desktop mode, return array of all open cards
const isMobile = Utils.getMobileMode();
const isMobile = Utils.isMiniScreen();
if (!isMobile) {
const openCardIds = Session.get('openCards') || [];
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
@ -123,7 +122,7 @@ BlazeComponent.extendComponent({
},
goHome() {
FlowRouter.go('home');
FlowRouter.go('home')
},
isConverting() {
@ -195,7 +194,7 @@ BlazeComponent.extendComponent({
}
},
onRendered() {
// Initialize user settings (zoom and mobile mode)
// Initialize user settings (mobile mode)
Utils.initializeUserSettings();
// Detect iPhone devices and add class for better CSS targeting
@ -221,9 +220,9 @@ BlazeComponent.extendComponent({
const popupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 &&
if (node.nodeType === 1 &&
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
!node.closest('.js-swimlanes') &&
!node.closest('.js-swimlanes') &&
!node.closest('.swimlane') &&
!node.closest('.list') &&
!node.closest('.minicard')) {
@ -391,23 +390,24 @@ BlazeComponent.extendComponent({
helper(evt, item) {
const helper = $(`<div class="swimlane"
style="flex-direction: column;
height: ${swimlaneWhileSortingHeight}px;
width: $(boardComponent.width)px;
overflow: hidden;"/>`);
max-height: 30vh;
width: 100vw;
overflow: hidden; z-index: 100;"/>`);
helper.append(item.clone());
// Also grab the list of lists of cards
const list = item.next();
helper.append(list.clone());
return helper;
},
items: '.swimlane:not(.placeholder)',
items: '.swimlane-container',
placeholder: 'swimlane placeholder',
distance: 7,
start(evt, ui) {
const listDom = ui.placeholder.next('.js-swimlane');
const parentOffset = ui.item.parent().offset();
ui.placeholder.height(ui.helper.height());
height = ui.helper.height();
ui.placeholder[0].setAttribute('style', `height: ${height}px !important;`);
EscapeActions.executeUpTo('popup-close');
listDom.addClass('moving-swimlane');
boardComponent.setIsDragging(true);
@ -415,40 +415,19 @@ BlazeComponent.extendComponent({
ui.placeholder.insertAfter(ui.placeholder.next());
boardComponent.origPlaceholderIndex = ui.placeholder.index();
// resize all swimlanes + headers to be a total of 150 px per row
// this could be achieved by setIsDragging(true) but we want immediate
// result
ui.item
.siblings('.js-swimlane')
.css('height', `${swimlaneWhileSortingHeight - 26}px`);
// set the new scroll height after the resize and insertion of
// the placeholder. We want the element under the cursor to stay
// at the same place on the screen
ui.item.parent().get(0).scrollTop =
ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
},
beforeStop(evt, ui) {
const parentOffset = ui.item.parent().offset();
const siblings = ui.item.siblings('.js-swimlane');
siblings.css('height', '');
// compute the new scroll height after the resize and removal of
// the placeholder
const scrollTop =
ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
// then reset the original view of the swimlane
siblings.removeClass('moving-swimlane');
// and apply the computed scrollheight
ui.item.parent().get(0).scrollTop = scrollTop;
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0);
const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0);
const prevSwimlaneDom = ui.item.prevAll('.swimlane-container').get(0);
const nextSwimlaneDom = ui.item.nextAll('.swimlane-container').get(0);
const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
$swimlanesDom.sortable('cancel');
@ -464,39 +443,7 @@ BlazeComponent.extendComponent({
boardComponent.setIsDragging(false);
},
sort(evt, ui) {
// get the mouse position in the sortable
const parentOffset = ui.item.parent().offset();
const cursorY =
evt.pageY - parentOffset.top + ui.item.parent().scrollTop();
// compute the intended index of the placeholder (we need to skip the
// slots between the headers and the list of cards)
const newplaceholderIndex = Math.floor(
cursorY / swimlaneWhileSortingHeight,
);
let destPlaceholderIndex = (newplaceholderIndex + 1) * 2;
// if we are scrolling far away from the bottom of the list
if (destPlaceholderIndex >= ui.item.parent().get(0).childElementCount) {
destPlaceholderIndex = ui.item.parent().get(0).childElementCount - 1;
}
// update the placeholder position in the DOM tree
if (destPlaceholderIndex !== ui.placeholder.index()) {
if (destPlaceholderIndex < boardComponent.origPlaceholderIndex) {
ui.placeholder.insertBefore(
ui.placeholder
.siblings()
.slice(destPlaceholderIndex - 2, destPlaceholderIndex - 1),
);
} else {
ui.placeholder.insertAfter(
ui.placeholder
.siblings()
.slice(destPlaceholderIndex - 1, destPlaceholderIndex),
);
}
}
Utils.scrollIfNeeded(evt);
},
});
@ -505,10 +452,10 @@ BlazeComponent.extendComponent({
dragscroll.reset();
if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
if (Utils.isMiniScreen()) {
$swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle');
} else {
$swimlanesDom.sortable('option', 'handle', '.swimlane-header');
$swimlanesDom.sortable('option', 'handle', '.swimlane-header-wrap');
}
// Disable drag-dropping if the current user is not a board member
@ -540,57 +487,57 @@ BlazeComponent.extendComponent({
isViewSwimlanes() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
// If no board view is set, default to swimlanes
if (!boardView) {
boardView = 'board-view-swimlanes';
}
return boardView === 'board-view-swimlanes';
},
isViewLists() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-lists';
},
isViewCalendar() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-cal';
},
isViewGantt() {
const currentUser = ReactiveCache.getCurrentUser();
let boardView;
if (currentUser) {
boardView = (currentUser.profile || {}).boardView;
} else {
boardView = window.localStorage.getItem('boardView');
}
return boardView === 'board-view-gantt';
},
@ -602,7 +549,7 @@ BlazeComponent.extendComponent({
}
return false;
}
try {
const swimlanes = currentBoard.swimlanes();
const hasSwimlanes = swimlanes && swimlanes.length > 0;
@ -638,7 +585,7 @@ BlazeComponent.extendComponent({
const isBoardReady = this.isBoardReady.get();
const isConverting = this.isConverting.get();
const boardView = Utils.boardView();
if (process.env.DEBUG === 'true') {
console.log('=== BOARD DEBUG STATE ===');
console.log('currentBoardId:', currentBoardId);
@ -648,7 +595,7 @@ BlazeComponent.extendComponent({
console.log('boardView:', boardView);
console.log('========================');
}
return {
currentBoardId,
hasCurrentBoard: !!currentBoard,
@ -1025,4 +972,3 @@ BlazeComponent.extendComponent({
* Gantt View Component
* Displays cards as a Gantt chart with start/due dates
*/

File diff suppressed because it is too large Load diff

View file

@ -22,918 +22,90 @@
padding: 0.7vh 0.7vw;
}
/* Zoom and Mobile Mode Controls */
.board-header-btns.center {
.board-header {
display: grid;
flex: 1;
gap: 0.3lh;
}
body {
&.mobile-mode {
.board-header {
flex-wrap: wrap;
}
}
&:not(.mobile-mode) {
.header-board-menu {
flex: 1;
}
.board-header {
justify-content: space-between;
grid-auto-flow: column;
}
.board-header-btns-left {
flex: 1;
justify-content: center;
}
.board-header-btns-right {
flex-grow: 0;
justify-content: end;
}
& .board-header-btns-right,
& .board-header-btns-left,
& .header-board-menu {
align-self: center;
align-items: center;
display: flex;
gap: 1.5ch;
overflow-wrap: normal;
}
}
}
/* Make some space on intermediate layouts */
@media screen and (max-width: 1200px) {
.board-header-btns-right span {
display: none !important;
}
}
.header-board-menu, .board-header-btns {
display: flex;
align-self: center;
align-items: center;
justify-content: center;
flex: 1;
gap: 1ch;
& p {
margin: 0;
}
}
.zoom-controls {
.board-header-btns-right > a {
flex-wrap: no-wrap;
}
body.mobile-mode {
header-board-menu h1 {
font-size: 2em;
}
.board-header-btn {
/* avoid wrapping if possible, at the cost of little icons */
font-size: 0.5em;
/* no much choice because the way FA icons are inserted */
padding-top: 0.1lh;
min-height: 0.8lh;
}
.board-header-btns-right {
display: grid;
grid-auto-flow: column;
grid-template-columns: repeat(auto-fit, 1fr);
flex: 1;
gap: 1ch;
justify-content: start;
align-items: center;
}
}
.board-header-btns-left {
display: flex;
align-items: center;
gap: 0.5vw;
background: rgba(255, 255, 255, 0.9);
padding: 0.5vh 1vw;
border-radius: 0.5vw;
box-shadow: 0 0.2vh 0.5vh rgba(0,0,0,0.1);
}
.zoom-controls .board-header-btn {
padding: 0.5vh 0.8vw !important;
border-radius: 0.3vw !important;
background: #fff !important;
border: 1px solid #000 !important;
transition: all 0.2s ease !important;
color: #000 !important;
height: auto !important;
line-height: normal !important;
margin: 0 !important;
float: none !important;
overflow: visible !important;
}
.zoom-controls .board-header-btn i {
color: #000 !important;
float: none !important;
display: inline !important;
line-height: normal !important;
margin: 0 !important;
}
.zoom-controls .board-header-btn:hover {
background: #000 !important;
border-color: #000 !important;
color: #fff !important;
}
.zoom-controls .board-header-btn:hover i {
color: #fff !important;
}
.zoom-controls .board-header-btn.is-active {
background: #0079bf;
color: white;
border-color: #005a8a;
}
.zoom-controls .board-header-btn.is-active i {
color: white;
}
.zoom-level {
font-weight: bold;
color: #333;
min-width: 3vw;
text-align: center;
font-size: clamp(12px, 2vw, 14px);
cursor: pointer;
padding: 0.3vh 0.5vw;
border-radius: 0.3vw;
transition: all 0.2s ease;
}
.zoom-level:hover {
background: #f0f0f0;
color: #000;
}
/* Mobile Mode Styles */
.mobile-mode .board-wrapper {
width: 100%;
height: 100%;
}
.mobile-mode .board-canvas {
height: 100%;
}
.mobile-mode .minicard {
font-size: clamp(16px, 4vw, 20px);
padding: 1.2vh 1.5vw 0.5vh;
min-height: 3vh;
}
.mobile-mode .list-header-name {
font-size: clamp(18px, 4.5vw, 24px);
}
.mobile-mode .board-header-btn {
padding: 1vh 1.5vw;
font-size: clamp(14px, 3.5vw, 18px);
}
.mobile-mode .zoom-controls {
padding: 1vh 1.5vw;
gap: 1vw;
}
.mobile-mode .zoom-controls .board-header-btn {
padding: 1vh 1.5vw !important;
font-size: clamp(14px, 3.5vw, 18px) !important;
background: #fff !important;
border: 1px solid #000 !important;
color: #000 !important;
height: auto !important;
line-height: normal !important;
margin: 0 !important;
float: none !important;
overflow: visible !important;
}
.mobile-mode .zoom-controls .board-header-btn i {
color: #000 !important;
float: none !important;
display: inline !important;
line-height: normal !important;
margin: 0 !important;
}
.mobile-mode .zoom-controls .board-header-btn:hover {
background: #000 !important;
border-color: #000 !important;
color: #fff !important;
}
.mobile-mode .zoom-controls .board-header-btn:hover i {
color: #fff !important;
}
.mobile-mode .zoom-level {
font-size: clamp(14px, 3.5vw, 18px);
min-width: 4vw;
}
/* Comprehensive Mobile Mode Styles - Works on all screen sizes */
.mobile-mode .board-wrapper {
width: 100% !important;
height: 100% !important;
transform: none !important;
transform-origin: initial !important;
max-width: 100% !important;
}
.mobile-mode .board-canvas {
height: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
width: 100% !important;
max-width: 100% !important;
}
.mobile-mode .swimlane {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
margin-bottom: 2rem !important;
display: block !important;
float: none !important;
}
.mobile-mode .swimlane-header {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: clamp(18px, 2.5vw, 32px) !important;
padding: 1rem !important;
margin-bottom: 1rem !important;
display: block !important;
}
.mobile-mode .list {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
display: block !important;
float: none !important;
margin-bottom: 2rem !important;
border-left: none !important;
border-bottom: 2px solid #ccc !important;
clear: both !important;
}
.mobile-mode .list-header {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 1rem !important;
font-size: clamp(18px, 2.5vw, 32px) !important;
display: grid !important;
grid-template-columns: 30px 1fr auto auto !important;
gap: 10px !important;
align-items: center !important;
position: relative !important;
}
.mobile-mode .list-header .list-header-name {
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
grid-row: 1 !important;
grid-column: 2 !important;
align-self: end !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-header .cardCount {
font-size: clamp(14px, 2vw, 24px) !important;
grid-row: 2 !important;
grid-column: 2 !important;
align-self: start !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-header .list-header-menu-icon {
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
grid-row: 1/3 !important;
grid-column: 3 !important;
padding: 14px !important;
font-size: clamp(24px, 3vw, 48px) !important;
text-align: center !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-header .list-header-handle {
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
grid-row: 1/3 !important;
grid-column: 4 !important;
padding: 14px !important;
font-size: clamp(28px, 3.5vw, 56px) !important;
text-align: center !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .list-body {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
padding: 1rem !important;
display: block !important;
}
.mobile-mode .minicard {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: clamp(16px, 2vw, 24px) !important;
padding: 1.2vh 1.5vw 0.5vh !important;
min-height: 3vh !important;
margin-bottom: 0.5rem !important;
display: block !important;
float: none !important;
}
.mobile-mode .minicard .minicard-title {
font-size: clamp(16px, 2vw, 24px) !important;
font-weight: bold !important;
}
.mobile-mode .minicard .minicard-members {
font-size: clamp(12px, 1.5vw, 18px) !important;
}
.mobile-mode .minicard .minicard-lists {
font-size: clamp(12px, 1.5vw, 18px) !important;
}
/* Desktop Mode Styles */
.desktop-mode .board-wrapper {
width: auto !important;
height: auto !important;
}
.desktop-mode .swimlane {
width: auto !important;
min-width: auto !important;
}
.desktop-mode .list {
width: auto !important;
min-width: auto !important;
display: flex !important;
float: left !important;
margin-bottom: 0 !important;
border-left: 1px solid #ccc !important;
border-bottom: none !important;
}
.desktop-mode .list-header {
width: auto !important;
min-width: auto !important;
padding: 2.5vh 1.5vw 0.5vh !important;
font-size: clamp(14px, 3vw, 18px) !important;
display: block !important;
}
.desktop-mode .list-header .list-header-name {
font-size: clamp(14px, 3vw, 18px) !important;
display: inline !important;
grid-row: auto !important;
grid-column: auto !important;
align-self: auto !important;
}
.desktop-mode .list-header .cardCount {
font-size: 12px !important;
grid-row: auto !important;
grid-column: auto !important;
align-self: auto !important;
}
.desktop-mode .list-header .list-header-menu-icon {
position: absolute !important;
right: 60px !important;
top: 50% !important;
transform: translateY(-50%) !important;
grid-row: auto !important;
grid-column: auto !important;
padding: 14px !important;
font-size: 40px !important;
}
.desktop-mode .list-header .list-header-handle {
position: absolute !important;
right: 10px !important;
top: 50% !important;
transform: translateY(-50%) !important;
grid-row: auto !important;
grid-column: auto !important;
padding: 7px !important;
font-size: clamp(16px, 3vw, 20px) !important;
}
.desktop-mode .list-body {
width: auto !important;
min-width: auto !important;
padding: 5px 11px !important;
}
.desktop-mode .minicard {
width: auto !important;
min-width: auto !important;
font-size: clamp(12px, 2.5vw, 16px) !important;
padding: 0.5vh 0.8vw !important;
min-height: auto !important;
margin-bottom: 9px !important;
}
.desktop-mode .minicard .minicard-title {
font-size: clamp(12px, 2.5vw, 16px) !important;
}
.desktop-mode .minicard .minicard-members {
font-size: 10px !important;
}
.desktop-mode .minicard .minicard-lists {
font-size: 10px !important;
}
/* Additional Mobile Mode Styles for Other Elements - Works on all screen sizes */
.mobile-mode .swimlane-header .swimlane-title {
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .swimlane-header .swimlane-description {
font-size: clamp(14px, 2vw, 24px) !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header {
font-size: clamp(18px, 2.5vw, 32px) !important;
padding: 1rem !important;
width: 100% !important;
max-width: 100% !important;
}
.mobile-mode .board-header .board-header-title {
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header .board-header-description {
font-size: clamp(14px, 2vw, 24px) !important;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header .board-header-btn {
font-size: clamp(14px, 2vw, 24px) !important;
padding: 1vh 1.5vw !important;
display: inline-block !important;
visibility: visible !important;
opacity: 1 !important;
}
.mobile-mode .board-header .board-header-btn i {
font-size: clamp(14px, 2vw, 24px) !important;
display: inline !important;
visibility: visible !important;
opacity: 1 !important;
}
/* Force mobile mode visibility on all screen sizes */
.mobile-mode .list-header .fa-angle-right,
.mobile-mode .list-header .fa-arrows {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
position: static !important;
right: auto !important;
top: auto !important;
transform: none !important;
}
.mobile-mode .list-header .fa-angle-right {
grid-row: 1/3 !important;
grid-column: 3 !important;
padding: 14px !important;
font-size: clamp(24px, 3vw, 48px) !important;
text-align: center !important;
}
.mobile-mode .list-header .fa-arrows {
grid-row: 1/3 !important;
grid-column: 4 !important;
padding: 14px !important;
font-size: clamp(28px, 3.5vw, 56px) !important;
text-align: center !important;
}
/* Override any media queries that might hide elements in mobile mode */
.mobile-mode * {
max-width: none !important;
}
.mobile-mode .list,
.mobile-mode .swimlane,
.mobile-mode .board-wrapper,
.mobile-mode .board-canvas {
max-width: 100% !important;
width: 100% !important;
min-width: 100% !important;
}
/* Force mobile mode list styling on all screen sizes - override desktop CSS */
.mobile-mode .board-canvas {
display: block !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.mobile-mode .swimlane {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
}
.mobile-mode .swimlane .swimlane-header {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
margin: 0 0 1rem 0 !important;
padding: 1rem !important;
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
border-bottom: 2px solid #ccc !important;
}
.mobile-mode .swimlane .lists {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
}
.mobile-mode .list {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
position: static !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
}
.mobile-mode .list:first-child {
margin-left: 0 !important;
margin-top: 0 !important;
}
.mobile-mode .list:last-child {
margin-right: 0 !important;
margin-bottom: 0 !important;
}
.mobile-mode .list.ui-sortable-helper {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
height: auto !important;
min-height: 60px !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
}
.mobile-mode .list.placeholder {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
height: auto !important;
min-height: 60px !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
}
/* Override any existing responsive CSS that might interfere with mobile mode */
.mobile-mode .board-canvas .swimlane .lists {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
overflow: visible !important;
}
.mobile-mode .board-canvas .swimlane .lists .list {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
position: static !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
}
/* Force mobile mode to override any media query styles */
@media screen and (min-width: 801px) {
.mobile-mode .board-canvas {
display: block !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.mobile-mode .swimlane {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
}
.mobile-mode .swimlane .lists {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
}
.mobile-mode .list {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
clear: both !important;
border-left: none !important;
border-right: none !important;
border-top: none !important;
border-bottom: 2px solid #ccc !important;
flex: none !important;
flex-basis: auto !important;
flex-grow: 0 !important;
flex-shrink: 0 !important;
position: static !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
}
}
/* Hide desktop-only elements in mobile mode (like mobile media queries do) */
.mobile-mode .board-header-btn i.fa + span {
display: none !important;
}
.mobile-mode .board-header-btn span {
display: none !important;
}
.mobile-mode .board-header-btn .fa + span {
display: none !important;
}
.mobile-mode .board-header-btn .fa + .board-header-btn-text {
display: none !important;
}
.mobile-mode .board-header-btn .fa + .board-header-btn-label {
display: none !important;
}
/* Show only icons in mobile mode */
.mobile-mode .board-header-btn {
width: auto !important;
min-width: auto !important;
padding: 8px !important;
text-align: center !important;
}
.mobile-mode .board-header-btn i {
display: inline-block !important;
margin: 0 !important;
}
/* Hide desktop-specific elements that shouldn't show in mobile mode */
.mobile-mode .desktop-only,
.mobile-mode .board-header .desktop-only {
display: none !important;
}
.mobile-mode .board-header .board-header-btn.desktop-only {
display: none !important;
}
/* Hide desktop-specific board header buttons in mobile mode */
.mobile-mode .board-header-btns.left {
display: none !important;
}
.mobile-mode .board-header-btns.center {
display: none !important;
}
/* Show only the right section buttons in mobile mode, but hide text labels */
.mobile-mode .board-header-btns.right {
display: block !important;
}
.mobile-mode .board-header-btns.right .board-header-btn span {
display: none !important;
}
.mobile-mode .board-header-btns.right .board-header-btn .fa + span {
display: none !important;
}
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-text {
display: none !important;
}
.mobile-mode .board-header-btns.right .board-header-btn .fa + .board-header-btn-label {
display: none !important;
}
/* Hide specific desktop-only buttons that shouldn't show in mobile mode */
.mobile-mode .board-header-btn.js-star-board span,
.mobile-mode .board-header-btn.js-change-visibility span,
.mobile-mode .board-header-btn.js-watch-board span,
.mobile-mode .board-header-btn.js-sort-cards span {
display: none !important;
}
/* Show only icons for mobile mode buttons */
.mobile-mode .board-header-btns.right .board-header-btn {
width: auto !important;
min-width: auto !important;
padding: 8px !important;
text-align: center !important;
margin: 0 2px !important;
}
.mobile-mode .board-header-btns.right .board-header-btn i {
display: inline-block !important;
margin: 0 !important;
}
/* Ensure mobile mode looks like small screen mobile view */
.mobile-mode .board-header {
height: 40px !important;
}
.mobile-mode .board-header .board-header-btns {
margin-top: 0px !important;
}
.mobile-mode .board-header .board-header-btn {
height: 32px !important;
line-height: 32px !important;
font-size: 15px !important;
}
.mobile-mode .board-header .board-header-btn i.fa {
line-height: 32px !important;
}
/* Copy mobile media query styles to mobile mode for consistent appearance */
.mobile-mode .board-header {
height: 40px !important;
padding: 0 !important;
margin: 0 !important;
}
.mobile-mode .board-header .board-header-btns {
margin-top: 0px !important;
height: 40px !important;
display: flex !important;
align-items: center !important;
justify-content: flex-end !important;
}
.mobile-mode .board-header .board-header-btn {
height: 32px !important;
line-height: 32px !important;
font-size: 15px !important;
margin: 0 2px !important;
padding: 4px 8px !important;
border-radius: 4px !important;
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
color: #fff !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
min-width: 32px !important;
width: auto !important;
}
.mobile-mode .board-header .board-header-btn:hover {
background: rgba(255, 255, 255, 0.2) !important;
border-color: rgba(255, 255, 255, 0.3) !important;
}
.mobile-mode .board-header .board-header-btn i.fa {
line-height: 32px !important;
font-size: 15px !important;
margin: 0 !important;
padding: 0 !important;
}
.mobile-mode .board-header .board-header-btn i.fa + span {
display: none !important;
}
.mobile-mode .board-header .board-header-btn span {
display: none !important;
}
/* Hide the board title in mobile mode to match mobile view */
.mobile-mode .header-board-menu {
display: none !important;
}
/* Ensure the board header takes full width in mobile mode */
.mobile-mode .board-header {
width: 100% !important;
max-width: 100% !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding: 0 10px !important;
}
/* Additional Desktop Mode Styles for Other Elements */
.desktop-mode .swimlane-header .swimlane-title {
font-size: clamp(14px, 3vw, 18px) !important;
}
.desktop-mode .swimlane-header .swimlane-description {
font-size: 12px !important;
}
.desktop-mode .board-header {
font-size: clamp(14px, 3vw, 18px) !important;
padding: 2.5vh 1.5vw 0.5vh !important;
}
.desktop-mode .board-header .board-header-title {
font-size: clamp(14px, 3vw, 18px) !important;
}
.desktop-mode .board-header .board-header-description {
font-size: 12px !important;
}
.desktop-mode .board-header .board-header-btn {
font-size: clamp(12px, 2.5vw, 16px) !important;
padding: 0.5vh 0.8vw !important;
}
.desktop-mode .board-header .board-header-btn i {
font-size: clamp(12px, 2.5vw, 16px) !important;
flex: 1;
gap: 2ch;
padding: 0 0.5ch;
}

View file

@ -1,26 +1,67 @@
template(name="boardHeaderBar")
h1.header-board-menu
with currentBoard
if $eq title 'Templates'
| {{_ 'templates'}}
else
+viewer
= title
.board-header
.header-board-menu
with currentBoard
if $eq title 'Templates'
| {{_ 'templates'}}
else
+viewer
= title
if currentBoard
if currentUser
with currentBoard
if currentUser.isBoardAdmin
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
unless isMiniScreen
.board-header-btns-left
if currentBoard
if currentUser
with currentBoard
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span {{_ currentBoard.permission}}
.board-header-btns.left
unless isMiniScreen
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}" title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span.board-star-counter {{currentBoard.stars}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}
.board-header-btns-right
.separator
if currentBoard
if currentUser
with currentBoard
if currentUser.isBoardAdmin
if isMiniScreen
if currentUser
with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
span {{_ currentBoard.permission}}
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
@ -29,86 +70,43 @@ template(name="boardHeaderBar")
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
span {{_ watchLevel}}
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
if showStarCounter
span.board-star-counter {{currentBoard.stars}}
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
span {{_ 'log-in'}}
.board-header-btns.center
.board-header-btns.right
if currentBoard
if isMiniScreen
if currentUser
with currentBoard
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
i.fa.fa-pencil-square-o
a.board-header-btn(
class="{{#if currentUser.isBoardAdmin}}js-change-visibility{{else}}is-disabled{{/if}}"
title="{{_ currentBoard.permission}}")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
a.board-header-btn.js-watch-board(
title="{{_ watchLevel }}")
if $eq watchLevel "watching"
i.fa.fa-eye
if $eq watchLevel "tracking"
i.fa.fa-bell
if $eq watchLevel "muted"
i.fa.fa-bell-slash
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
a.board-header-btn(title="{{_ 'sort-cards'}}" class="{{#if isSortActive }}emphasis{{else}} js-sort-cards {{/if}}")
i.fa.fa-sort
span {{#if isSortActive }}{{_ 'sort-is-on'}}{{else}}{{_ 'sort-cards'}}{{/if}}
if isSortActive
a.board-header-btn-close.js-sort-reset(title="{{_ 'remove-sort'}}")
i.fa.fa-times-thin
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
else
a.board-header-btn.js-log-in(
title="{{_ 'log-in'}}")
i.fa.fa-sign-in
if isSandstorm
if currentUser
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
if isSandstorm
if currentUser
a.js-open-archived-board
i.fa.fa-archive
//if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
// i.fa(class="{{directionClass}}")
// span {{_ 'sort'}}{{_ listSortShortDesc}}
//if showSort
// a.board-header-btn.js-open-sort-view(title="{{_ 'sort-desc'}}")
// i.fa(class="{{directionClass}}")
// span {{_ 'sort'}}{{_ listSortShortDesc}}
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
i.fa.fa-filter
span {{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
a.board-header-btn.js-open-filter-view(
title="{{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}"
class="{{#if Filter.isActive}}js-filter-active{{/if}}")
i.fa.fa-filter
span {{#if Filter.isActive}}{{_ 'filter-on-desc'}}{{else}}{{_ 'filter'}}{{/if}}
if Filter.isActive
a.board-header-btn-close.js-filter-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search
span {{_ 'search'}}
a.board-header-btn.js-open-search-view(title="{{_ 'search'}}")
i.fa.fa-search
span {{_ 'search'}}
unless currentBoard.isTemplatesBoard
a.board-header-btn.js-toggle-board-view
@ -140,9 +138,9 @@ template(name="boardHeaderBar")
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
.separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-bars
.separator
a.board-header-btn.js-toggle-sidebar(title="{{_ 'sidebar-open'}} {{_ 'or'}} {{_ 'sidebar-close'}}")
i.fa.fa-bars
template(name="boardVisibilityList")
ul.pop-over-list
@ -172,26 +170,29 @@ template(name="boardChangeWatchPopup")
li
with "watching"
a.js-select-watch
i.fa.fa-eye
| {{_ 'watching'}}
if watchCheck
i.fa.fa-check
span
i.fa.fa-eye
| {{_ 'watching'}}
if watchCheck
i.fa.fa-check
span.sub-name {{_ 'watching-info'}}
li
with "tracking"
a.js-select-watch
i.fa.fa-bell
| {{_ 'tracking'}}
if watchCheck
i.fa.fa-check
span
i.fa.fa-bell
| {{_ 'tracking'}}
if watchCheck
i.fa.fa-check
span.sub-name {{_ 'tracking-info'}}
li
with "muted"
a.js-select-watch
i.fa.fa-bell-slash
| {{_ 'muted'}}
if watchCheck
i.fa.fa-check
span
i.fa.fa-bell-slash
| {{_ 'muted'}}
if watchCheck
i.fa.fa-check
span.sub-name {{_ 'muted-info'}}
template(name="boardChangeViewPopup")
@ -247,12 +248,13 @@ template(name="createBoard")
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
.create-element-foooter
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
template(name="createBoardPopup")
form
@ -276,12 +278,13 @@ template(name="createBoardPopup")
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
.create-element-foooter
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
// New popup for Template Container creation; shares the same form content
template(name="createTemplateContainerPopup")
@ -305,13 +308,14 @@ template(name="createTemplateContainerPopup")
a.flex.js-toggle-add-template-container
.materialCheckBox#add-template-container
span {{_ 'add-template-container'}}
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
.create-element-foooter
input.primary.wide(type="submit" value="{{_ 'create'}}")
span.quiet
| {{_ 'or'}}
a.js-import-board {{_ 'import'}}
span.quiet
| /
a.js-board-template {{_ 'template'}}
//template(name="listsortPopup")
// h2
@ -362,4 +366,3 @@ template(name="cardsSortPopup")
a.js-sort-created-asc
i.fa.fa-arrow-up
| {{_ 'created-at-oldest-first'}}

View file

@ -33,9 +33,6 @@ BlazeComponent.extendComponent({
const currentBoard = Utils.getCurrentBoard();
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
},
isStarred() {
const boardId = Session.get('currentBoard');
const user = ReactiveCache.getCurrentUser();
@ -182,7 +179,7 @@ Template.boardHeaderBar.helpers({
if (!sortBy) {
return '🃏'; // Card icon when nothing is selected
}
// Determine which sort option is active based on sortBy object
if (sortBy.dueAt) {
return '📅'; // Due date icon
@ -191,7 +188,7 @@ Template.boardHeaderBar.helpers({
} else if (sortBy.createdAt) {
return sortBy.createdAt === 1 ? '⬆️' : '⬇️'; // Up/down arrow based on direction
}
return '🃏'; // Default card icon
},
});

File diff suppressed because it is too large Load diff

View file

@ -67,81 +67,71 @@ template(name="boardList")
// Right boards grid
.boards-right-grid
.boards-path-header
.path-left
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
if BoardMultiSelection.isActive
span.multiselection-hint
span.emoji-icon
i.fa.fa-thumb-tack
| {{_ 'multi-selection-active'}}
.path-right
if canModifyBoards
if hasBoardsSelected
button.js-archive-selected-boards.board-header-btn
.path-bottom
.path-left
span.path-icon.emoji-icon {{currentMenuPath.icon}}
span.path-text {{currentMenuPath.text}}
.path-right
unless isMiniScreen
+headerMultiSelection
if canModifyBoards
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
span.emoji-icon
i.fa.fa-archive
span {{_ 'archive-board'}}
button.js-duplicate-selected-boards.board-header-btn
span.emoji-icon
i.fa.fa-clipboard
span {{_ 'duplicate-board'}}
a.board-header-btn.js-multiselection-activate(
title="{{#if BoardMultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}"
class="{{#if BoardMultiSelection.isActive}}emphasis{{/if}}")
span.emoji-icon
i.fa.fa-check-square-o
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
i.fa.fa-check-square-o
if BoardMultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
span.emoji-icon
i.fa.fa-times
ul.board-list.clearfix.js-boards(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if BoardMultiSelection.isActive}}is-multiselection-active{{/if}}")
li.js-add-board
if isSelectedMenu 'templates'
a.board-list-item.label(title="{{_ 'add-template-container'}}")
span.emoji-icon
i.fa.fa-plus
| &nbsp;{{_ 'add-template-container'}}
| {{_ 'add-template-container'}}
else
a.board-list-item.label(title="{{_ 'add-board'}}")
span.emoji-icon
i.fa.fa-plus
| &nbsp;{{_ 'add-board'}}
| {{_ 'add-board'}}
each boards
li.js-board(class="{{_id}} {{#if isStarred}}starred{{/if}} {{colorClass}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}", draggable="true")
if isInvited
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.details
span.board-list-item-name= title
.board-card-header
span.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
| {{#if isStarred}}⭐{{else}}☆{{/if}}
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
.board-card-body
span.details
span.board-list-item-name= title
p.board-list-item-desc {{_ 'just-invited'}}
button.js-accept-invite.primary {{_ 'accept'}}
button.js-decline-invite {{_ 'decline'}}
.board-card-footer
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isActive }}active{{/if}} {{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
else
if $eq type "template-container"
.template-container.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon
i.fa.fa-arrows
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
.template-container.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.details
span.board-list-item-name(title="{{_ 'template-container'}}")
+viewer
= title
p.board-list-item-desc
+viewer
= description
//- #FIXME: is this obsolete ?
//- p.board-list-item-desc
//- +viewer
//- = description
if hasSpentTimeCards
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
@ -154,19 +144,20 @@ template(name="boardList")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
else
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.board-handle(title="{{_ 'drag-board'}}")
span.emoji-icon
i.fa.fa-arrows
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
.board-list-item
if BoardMultiSelection.isActive
.materialCheckBox.multi-selection-checkbox.js-toggle-board-multi-selection(
class="{{#if BoardMultiSelection.isSelected _id}}is-checked{{/if}}")
span.details
span.board-list-item-name(title="{{_ 'board-drag-drop-reorder-or-click-open'}}")
+viewer
= title
//- p.board-list-item-desc
//- +viewer
//- = description
unless currentSetting.hideBoardMemberList
if allowsBoardMemberList
.minicard-members
@ -175,34 +166,24 @@ template(name="boardList")
+userAvatar(userId=member noRemove=true)
unless currentSetting.hideCardCounterList
if allowsCardCounterList
.minicard-lists.flex.flex-wrap
.minicard-lists
each list in boardLists _id
.item
| {{ list }}
p.board-list-item-desc
+viewer
= description
if hasSpentTimeCards
span.js-has-spenttime-cards(
class="{{#if hasOvertimeCards}}has-overtime-card-active{{else}}no-overtime-card-active{{/if}}"
title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}")
span.emoji-icon
i.fa.fa-clock-o
a.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
a.js-star-board(
class="{{#if isStarred}}is-star-active{{else}}is-not-star-active{{/if}}"
title="{{_ 'star-board-title'}}")
span.emoji-icon
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
template(name="boardListHeaderBar")
h1 {{_ title }}
//.board-header-btns.right
// a.board-header-btn.js-open-archived-board
// i.fa.fa-archive
// span {{_ 'archives'}}
// a.board-header-btn(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
// i.fa.fa-clone
// span {{_ 'templates'}}
// Recursive template for workspaces tree
template(name="workspaceTree")
@ -214,7 +195,7 @@ template(name="workspaceTree")
span.workspace-drag-handle
span.emoji-icon
i.fa.fa-arrows
a.js-select-workspace(data-id="{{id}}")
span.workspace-icon
if icon
@ -231,3 +212,16 @@ template(name="workspaceTree")
a.js-add-subworkspace(data-id="{{id}}" title="{{_ 'allboards.add-subworkspace'}}") +
if children
+workspaceTree(nodes=children selectedWorkspaceId=selectedWorkspaceId)
template(name="headerMultiSelection")
if BoardMultiSelection.isActive
if canModifyBoards
if hasBoardsSelected
button.negate.js-archive-selected-boards.board-header-btn
span.emoji-icon
i.fa.fa-archive
span {{_ 'archive-board'}}
button.negate.js-duplicate-selected-boards.board-header-btn
span.emoji-icon
i.fa.fa-clipboard
span {{_ 'duplicate-board'}}

View file

@ -108,10 +108,7 @@ BlazeComponent.extendComponent({
const newTree = EJSON.clone(tree);
// Remove the dragged space
const { tree: treeAfterRemoval, removed } = removeSpace(
newTree,
draggedSpaceId,
);
const { tree: treeAfterRemoval, removed } = removeSpace(newTree, draggedSpaceId);
if (removed) {
// Insert after target
@ -127,46 +124,39 @@ BlazeComponent.extendComponent({
onRendered() {
// jQuery sortable is disabled in favor of HTML5 drag-and-drop for space management
// The old sortable code has been removed to prevent conflicts
/* OLD SORTABLE CODE - DISABLED
const itemsSelector = '.js-board:not(.placeholder)';
const $boards = this.$('.js-boards');
$boards.sortable({
connectWith: '.js-boards',
tolerance: 'pointer',
appendTo: '.board-list',
helper: 'clone',
distance: 7,
items: itemsSelector,
placeholder: 'board-wrapper placeholder',
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
},
async stop(evt, ui) {
const prevBoardDom = ui.item.prev('.js-board').get(0);
const nextBoardDom = ui.item.next('.js-board').get(0);
const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
// #FIXME OLD SORTABLE CODE - WILL BE DISABLED
//
// const itemsSelector = '.js-board';
const boardDomElement = ui.item.get(0);
const board = Blaze.getData(boardDomElement);
$boards.sortable('cancel');
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
await currentUser.setBoardSortIndex(board._id, sortIndex.base);
}
},
});
// const $boards = this.$('.js-boards');
// $boards.sortable({
// connectWith: '.js-boards',
// tolerance: 'pointer',
// appendTo: '.board-list',
// helper: 'clone',
// distance: 7,
// items: itemsSelector,
// placeholder: 'board-wrapper placeholder',
// start(evt, ui) {
// ui.helper.css('z-index', 1000);
// ui.placeholder.height(ui.helper.height());
// EscapeActions.executeUpTo('popup-close');
// },
// async stop(evt, ui) {
// const prevBoardDom = ui.item.prev('.js-board').get(0);
// const nextBoardDom = ui.item.next('.js-board').get(0);
// const sortIndex = Utils.calculateIndex(prevBoardDom, nextBoardDom, 1);
this.autorun(() => {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$boards.sortable({
handle: '.board-handle',
});
}
});
*/
// const boardDomElement = ui.item.get(0);
// const board = Blaze.getData(boardDomElement);
// $boards.sortable('cancel');
// const currentUser = ReactiveCache.getCurrentUser();
// if (currentUser && typeof currentUser.setBoardSortIndex === 'function') {
// await currentUser.setBoardSortIndex(board._id, sortIndex.base);
// }
// },
// });
},
userHasTeams() {
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) return true;
@ -357,7 +347,7 @@ BlazeComponent.extendComponent({
const lists = ReactiveCache.getLists({ 'boardId': boardId, 'archived': false },{sort: ['sort','asc']});
const ret = lists.map(list => {
let cardCount = ReactiveCache.getCards({ 'boardId': boardId, 'listId': list._id }).length;
return `${list.title}: ${cardCount}`;
return `${list.title}: ${cardCountcardCount}`;
});
return ret;
*/
@ -535,6 +525,7 @@ BlazeComponent.extendComponent({
'click .js-multiselection-reset'(evt) {
evt.preventDefault();
BoardMultiSelection.disable();
Popup.close();
},
'click .js-toggle-board-multi-selection'(evt) {
evt.preventDefault();
@ -708,6 +699,7 @@ BlazeComponent.extendComponent({
icon: newIcon || '📁',
});
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
if (err) console.error(err);
});
@ -808,6 +800,7 @@ BlazeComponent.extendComponent({
// Get the workspace ID directly from the dropped workspace-node's data-workspace-id attribute
const workspaceId = targetEl.getAttribute('data-workspace-id');
if (workspaceId) {
if (isMultiBoard) {
// Multi-board drag
@ -830,6 +823,7 @@ BlazeComponent.extendComponent({
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
// Only allow drop on "remaining" menu to unassign boards from spaces
if (menuType === 'remaining') {
@ -844,9 +838,11 @@ BlazeComponent.extendComponent({
evt.preventDefault();
evt.stopPropagation();
const menuType = evt.currentTarget.getAttribute('data-type');
evt.currentTarget.classList.remove('drag-over');
// Only handle drops on "remaining" menu
if (menuType !== 'remaining') return;
@ -908,6 +904,7 @@ BlazeComponent.extendComponent({
};
const allBoards = ReactiveCache.getBoards(query, {});
if (type === 'starred') {
return allBoards.filter(
(b) => currentUser && currentUser.hasStarred(b._id),

View file

@ -23,7 +23,7 @@
.original-positions-content {
background-color: white;
border: 1px solid #dee2e6;
border-radius: 4px;
border-radius: 0.4ch;
padding: 15px;
}
@ -65,7 +65,7 @@
.original-position-item {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
border-radius: 0.4ch;
margin-bottom: 10px;
padding: 12px;
transition: all 0.2s ease;
@ -100,7 +100,7 @@
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
}
@ -112,7 +112,7 @@
.entity-id {
color: #6c757d;
font-size: 11px;
font-family: monospace;
}
@ -123,12 +123,12 @@
.original-position-description {
color: #495057;
margin-bottom: 6px;
font-size: 13px;
}
.original-title {
color: #6c757d;
font-size: 12px;
margin-bottom: 6px;
padding: 4px 6px;
background-color: #e9ecef;
@ -141,7 +141,7 @@
.original-position-date {
color: #6c757d;
font-size: 11px;
}
.no-original-positions {
@ -152,7 +152,7 @@
}
.no-original-positions i {
font-size: 24px;
margin-bottom: 10px;
display: block;
color: #adb5bd;
@ -164,32 +164,32 @@
margin: 5px 0;
padding: 10px;
}
.original-positions-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.original-positions-header .btn {
justify-content: center;
}
.original-positions-filters .btn-group {
justify-content: center;
}
.original-position-item-header {
flex-wrap: wrap;
gap: 6px;
}
.entity-name {
flex: 1;
min-width: 0;
word-break: break-word;
}
.original-position-item-details {
margin-left: 0;
margin-top: 8px;
@ -203,60 +203,60 @@
border-color: #4a5568;
color: #e2e8f0;
}
.original-positions-content {
background-color: #1a202c;
border-color: #4a5568;
}
.original-position-item {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-item:hover {
background-color: #4a5568;
border-color: #718096;
}
.original-position-item-header {
color: #e2e8f0;
}
.original-position-item-header i {
color: #a0aec0;
}
.entity-name {
color: #e2e8f0;
}
.entity-id {
color: #a0aec0;
}
.original-position-description {
color: #e2e8f0;
}
.original-title {
background-color: #4a5568;
color: #a0aec0;
}
.original-title strong {
color: #e2e8f0;
}
.original-position-date {
color: #a0aec0;
}
.no-original-positions {
color: #a0aec0;
}
.no-original-positions i {
color: #718096;
}

View file

@ -6,75 +6,80 @@
font-weight: bold;
}
.attachment-gallery {
display: flex;
flex-direction: column;
display: grid;
grid-auto-flow: row;
}
.attachment-item {
display: flex;
flex-direction: row;
display: grid;
grid-template-columns: 10ch auto;
align-items: center;
margin-top: 16px;
grid-template-rows: repeat(auto-fit, minmax(1.5lh, auto));
justify-content: stretch;
gap: 2ch;
padding: 2ch;
border-radius: 0.6ch;
}
.attachment-item:hover {
background: #e0e0e0;
}
.attachment-thumbnail-container {
display: block;
width: 150px;
min-width: 150px;
max-height: 150px;
padding-right: 16px;
.attachment-details-container {
display: flex;
flex: 1;
}
.attachment-thumbnail-container {
display: flex;
flex: 1;
position: relative;
}
.attachment-thumbnail {
max-width: 150px;
max-height: 150px;
min-height: 2em;
/* more deterministic outcome */
aspect-ratio: 1/1;
object-fit: cover;
max-width: 100%;
cursor: pointer;
border-radius: 0.4ch;
}
.attachment-thumbnail-text {
min-height: 2em;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
cursor: pointer;
flex: 1;
text-align: center;
border-radius: 2px;
border: 1px solid #ccc;
border-radius: 5px;
}
.attachment-details-container {
display: block;
flex-grow: 1;
}
.attachment-details {
display: flex;
justify-content: space-between;
margin-right: 25px; /* Make sure the icons are not to far to the right */
flex: 1;
gap: 0.5ch;
align-items: center;
}
.attachment-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 1.5ch;
}
.attachment-actions a {
margin-left: 16px;
}
.attachment-actions a:first-child {
margin-left: 0;
body.mobile-mode .attachment-actions {
flex-direction: column;
gap: 0;
}
.add-attachment {
border: 1px dashed #555;
border-radius: .5ch;
cursor: pointer;
aspect-ratio: 1/1;
height: 1.5lh;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #555;
border-radius: 5px;
padding: 10px;
cursor: pointer;
margin-top: 16px;
}
.icon {
font-size: 1.5em;
cursor: pointer;
margin-left: 10px;
}
.icon:hover {
color: #666;
@ -95,26 +100,25 @@
height: 100%;
}
#viewer-top-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
padding: 16px;
display: grid;
grid-template-columns: 1fr auto;
justify-content: center;
justify-items: center;
font-size: 2rem;
padding: 0.3lh 0.5ch;
}
#attachment-name {
color: white;
font-size: 1.5em;
max-width: calc(
100% - 50px
); /* Make sure the name does not overlap the close button */
text-overflow: ellipsis;
overflow: hidden;
}
#viewer-close {
color: white;
cursor: pointer;
font-size: 4em;
position: absolute;
right: 50px;
top: 16px;
font-size: 2em;
}
/* Upload progress indicators for drag-and-drop uploads */
@ -122,30 +126,24 @@
.card-details-upload-progress {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 12px;
margin: 8px 0;
font-size: 14px;
border-radius: 0.4ch;
}
.upload-progress-header {
display: flex;
align-items: center;
margin-bottom: 8px;
font-weight: bold;
color: #495057;
}
.upload-progress-header i {
margin-right: 8px;
color: #007bff;
}
.upload-progress-item {
display: flex;
flex-direction: column;
margin-bottom: 8px;
padding: 8px;
background: white;
border-radius: 3px;
border: 1px solid #dee2e6;
@ -158,22 +156,17 @@
.upload-progress-filename {
font-weight: 500;
margin-bottom: 4px;
color: #495057;
word-break: break-all;
}
.upload-progress-bar {
width: 100%;
height: 6px;
background: #e9ecef;
border-radius: 3px;
overflow: hidden;
margin-bottom: 4px;
}
.upload-progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
border-radius: 3px;
@ -187,7 +180,6 @@
.upload-progress-success {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 500;
}
@ -199,47 +191,6 @@
color: #28a745;
}
.upload-progress-error i,
.upload-progress-success i {
margin-right: 4px;
}
/* Minicard specific styles */
.minicard-upload-progress {
margin: 4px 0;
padding: 8px;
font-size: 12px;
}
.minicard-upload-progress .upload-progress-item {
padding: 6px;
margin-bottom: 6px;
}
.minicard-upload-progress .upload-progress-filename {
font-size: 11px;
}
/* Card details specific styles */
.card-details-upload-progress {
margin: 12px 0;
padding: 16px;
}
.card-details-upload-progress .upload-progress-header {
font-size: 16px;
margin-bottom: 12px;
}
.card-details-upload-progress .upload-progress-item {
padding: 12px;
margin-bottom: 10px;
}
.card-details-upload-progress .upload-progress-filename {
font-size: 14px;
}
/* Drag over state for minicards */
.minicard.is-dragging-over {
border: 2px dashed #007bff !important;
@ -256,7 +207,6 @@
color: white;
cursor: pointer;
align-self: center;
margin: 0 20px;
}
#prev-attachment {
font-size: 4em;
@ -322,7 +272,6 @@
position: absolute;
bottom: 2.2em;
font-size: 1.6em;
padding: 16px;
}
#prev-attachment {
left: 0;
@ -356,19 +305,10 @@
margin-top: 20%;
width: 100%;
}
.attachment-thumbnail-container {
width: 100px;
min-width: 100px;
}
.attachment-thumbnail {
max-width: 100px;
}
.attachment-details {
flex-direction: column;
margin-right: 0px;
}
.attachment-actions {
flex-direction: row;
margin-top: 10px;
}
}

View file

@ -49,15 +49,11 @@ template(name="attachmentViewer")
i.fa.fa-caret-right#next-attachment
template(name="attachmentGallery")
if canModifyCard
a.add-attachment.js-add-attachment
i.fa.fa-plus
.attachment-gallery
if canModifyCard
a.attachment-item.add-attachment.js-add-attachment
i.fa.fa-plus
each attachments
.attachment-item(class="{{#if isAttachmentMigrating _id}}migrating{{/if}}")
.attachment-thumbnail-container.open-preview(data-attachment-id="{{_id}}" data-card-id="{{ meta.cardId }}")
if link

View file

@ -1,6 +1,5 @@
.card-date {
display: block;
border-radius: 4px;
padding: 1px 3px;
background-color: #dbdbdb;
}
@ -106,6 +105,10 @@
background-color: #e6c200;
}
.date a:has(time) {
text-decoration: none;
}
.card-date.end-date {
background-color: #ffb3b3; /* Light red for end */
color: #000; /* Black text for end */
@ -139,6 +142,6 @@
}
.customfield-date {
display: block;
border-radius: 4px;
border-radius: 0.4ch;
padding: 1px 3px;
}

View file

@ -1,24 +1,24 @@
import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker';
import {
formatDateTime,
formatDate,
import {
formatDateTime,
formatDate,
formatDateByUserPreference,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar,
diff
} from '/imports/lib/dateUtils';
@ -143,7 +143,7 @@ class CardReceivedDate extends CardDate {
const startAt = this.data().getStart();
const theDate = this.date.get();
const now = this.now.get();
// Received date logic: if received date is after start, due, or end dates, it's overdue
if (
(startAt && isAfter(theDate, startAt)) ||
@ -187,7 +187,7 @@ class CardStartDate extends CardDate {
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// Start date logic: if start date is after due or end dates, it's overdue
if ((endAt && isAfter(theDate, endAt)) || (dueAt && isAfter(theDate, dueAt))) {
classes += 'overdue';
@ -230,7 +230,7 @@ class CardDueDate extends CardDate {
const endAt = this.data().getEnd();
const theDate = this.date.get();
const now = this.now.get();
// If there's an end date and it's before the due date, task is completed early
if (endAt && isBefore(endAt, theDate)) {
classes += 'completed-early';
@ -242,7 +242,7 @@ class CardDueDate extends CardDate {
// Due date logic based on current time
else {
const daysDiff = diff(theDate, now, 'days');
if (daysDiff < 0) {
// Due date is in the past - overdue
classes += 'overdue';
@ -254,7 +254,7 @@ class CardDueDate extends CardDate {
classes += 'not-due';
}
}
return classes;
}
@ -286,7 +286,7 @@ class CardEndDate extends CardDate {
let classes = 'end-date ';
const dueAt = this.data().getDue();
const theDate = this.date.get();
if (!dueAt) {
// No due date set - just show as completed
classes += 'completed';
@ -371,7 +371,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardReceivedDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -383,7 +383,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardStartDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -395,7 +395,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardDueDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -407,7 +407,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardEndDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
@ -419,7 +419,7 @@ CardCustomFieldDate.register('cardCustomFieldDate');
template() {
return 'minicardCustomFieldDate';
}
showDate() {
const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';

View file

@ -1,16 +1,12 @@
.new-description {
position: relative;
margin: 0 0 20px 0;
flex: 1;
}
.new-description.is-open .helper {
display: inline-block;
}
.new-description.is-open textarea {
min-height: 100px;
.new-description textarea {
min-height: 1lh;
color: #4d4d4d;
cursor: auto;
overflow: hidden;
word-wrap: break-word;
overflow-wrap: break-word;
}
.new-description .too-long {
margin-top: 8px;
@ -19,9 +15,6 @@
background-color: #fff;
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
height: 36px;
margin: 4px 4px 6px 0;
padding: 9px 11px;
width: 100%;
}
.new-description textarea:hover,
@ -39,16 +32,12 @@
border: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.23);
color: #8c8c8c;
height: 36px;
margin: 4px 4px 6px 0;
width: 92%;
}
.description-item:hover {
background: #e0e0e0;
}
.description-item.add-description {
display: flex;
margin: 5px;
}
.description-item.add-description a {
display: block;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2,25 +2,25 @@ import { ReactiveCache } from '/imports/reactiveCache';
import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { DatePicker } from '/client/lib/datepicker';
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 Cards from '/models/cards';
import Boards from '/models/boards';
@ -35,6 +35,7 @@ import { DialogWithBoardSwimlaneList } from '/client/lib/dialogWithBoardSwimlane
import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwimlaneListCard';
import { handleFileUpload } from './attachments';
import uploadProgressManager from '../../lib/uploadProgressManager';
import PopupComponent from '../main/popup';
const subManager = new SubsManager();
const { calculateIndexData } = Utils;
@ -60,19 +61,8 @@ BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Utils.getCurrentBoard();
this.isLoaded = new ReactiveVar(false);
this.dep = new Tracker.Dependency();
if (this.parentComponent() && this.parentComponent().parentComponent()) {
const boardBody = this.parentComponent().parentComponent();
//in Miniview parent is Board, not BoardBody.
if (boardBody !== null) {
// Only show overlay in mobile mode, not in desktop mode
const isMobile = Utils.getMobileMode();
if (isMobile) {
boardBody.showOverlay.set(true);
}
boardBody.mouseHasEnterCardDetails = false;
}
}
this.calculateNextPeak();
Meteor.subscribe('unsaved-edits');
@ -85,6 +75,18 @@ BlazeComponent.extendComponent({
// });
},
onRendered() {
const boardOverlay = document.getElementsByClassName('board-overlay')?.[0];
this.boardBody = BlazeComponent.getComponentForElement(boardOverlay);
if (this.boardBody) {
this.boardBody.mouseHasEnterCardDetails = false;
}
const isMobile = Utils.getMobileMode();
if (isMobile && Session.get('currentCard')) {
//this.boardBody?.showOverlay.set(true);
}
},
isWatching() {
const card = this.currentData();
if (!card || typeof card.findWatcher !== 'function') return false;
@ -95,8 +97,8 @@ BlazeComponent.extendComponent({
return ReactiveCache.getCurrentUser().hasCustomFieldsGrid();
},
cardMaximized() {
this.dep.depend();
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
},
@ -175,6 +177,11 @@ BlazeComponent.extendComponent({
},
onRendered() {
// #FIXME hackish; if accepted tweak static funcs
if (this.cardMaximized()) {
PopupComponent.maximize({target: this.firstNode()});
}
if (Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED) {
// Send Webhook but not create Activities records ---
const card = this.currentData();
@ -209,11 +216,11 @@ BlazeComponent.extendComponent({
}
const $checklistsDom = this.$('.card-checklist-items');
const sortableSelector = Utils.isMiniScreen() ? '.checklist-handle' : '.checklist-title';
$checklistsDom.sortable({
tolerance: 'pointer',
helper: 'clone',
handle: '.checklist-title',
handle: sortableSelector,
items: '.js-checklist',
placeholder: 'checklist placeholder',
distance: 7,
@ -282,6 +289,8 @@ BlazeComponent.extendComponent({
return ReactiveCache.getCurrentUser()?.isBoardMember();
}
// Disable sorting if the current user is not a board member
this.autorun(() => {
const disabled = !userIsMember();
@ -289,10 +298,7 @@ BlazeComponent.extendComponent({
$checklistsDom.data('uiSortable') ||
$checklistsDom.data('sortable')
) {
$checklistsDom.sortable('option', 'disabled', disabled);
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$checklistsDom.sortable({ handle: '.checklist-handle' });
}
$checklistsDom.sortable('option', 'handle', sortableSelector);
}
if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
$subtasksDom.sortable('option', 'disabled', disabled);
@ -301,11 +307,7 @@ BlazeComponent.extendComponent({
},
onDestroyed() {
if (this.parentComponent() === null) return;
const parentComponent = this.parentComponent().parentComponent();
//on mobile view parent is Board, not board body.
if (parentComponent === null) return;
parentComponent.showOverlay.set(false);
this.boardBody?.showOverlay.set(false);
},
events() {
@ -332,59 +334,11 @@ BlazeComponent.extendComponent({
},
'mousedown .js-card-drag-handle'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
PopupComponent.toFront(event);
},
'mousedown .js-card-title-drag-handle'(event) {
// Allow dragging from title for ReadOnly users
// Don't interfere with text selection
if (event.target.tagName === 'A' || $(event.target).closest('a').length > 0) {
return; // Don't drag if clicking on links
}
'click .js-card-send-to-back'(event) {
event.preventDefault();
const $card = $(event.target).closest('.card-details');
const startX = event.clientX;
const startY = event.clientY;
const startLeft = $card.offset().left;
const startTop = $card.offset().top;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
$card.css({
left: startLeft + deltaX + 'px',
top: startTop + deltaY + 'px'
});
};
const onMouseUp = () => {
$(document).off('mousemove', onMouseMove);
$(document).off('mouseup', onMouseUp);
};
$(document).on('mousemove', onMouseMove);
$(document).on('mouseup', onMouseUp);
PopupComponent.toBack(event);
},
'click .js-close-card-details'() {
// Get board ID from either the card data or current board in session
@ -392,26 +346,21 @@ BlazeComponent.extendComponent({
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
const cardId = card && card._id;
if (boardId) {
// In desktop mode, remove from openCards array
const isMobile = Utils.getMobileMode();
if (!isMobile && cardId) {
const openCards = Session.get('openCards') || [];
const filtered = openCards.filter(id => id !== cardId);
Session.set('openCards', filtered);
// If this was the current card, clear it
if (Session.get('currentCard') === cardId) {
Session.set('currentCard', null);
}
// Don't navigate away in desktop mode - just close the card
return;
if (boardId && cardId) {
const openCards = Session.get('openCards') || [];
const filtered = openCards.filter(id => id !== cardId);
// If this was the current card, clear it
if (openCards.length === filtered.length) {
Session.set('currentCard', null);
}
else {
Session.set('currentCard', filtered[0]);
}
Session.set('openCards', filtered);
// Mobile mode: Clear the current card session to close the card
Session.set('currentCard', null);
// Navigate back to board without card
// Navigate back to board without card: must be done at the time of writing
// otherwise the route for the card is disabled until another
// card is opened
const board = ReactiveCache.getBoard(boardId);
if (board) {
FlowRouter.go('board', {
@ -434,34 +383,6 @@ BlazeComponent.extendComponent({
Meteor.call('changeDateFormat', dateFormat);
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
// Mobile: switch to desktop popup view (maximize)
'click .js-mobile-switch-to-desktop'(event) {
event.preventDefault();
// Switch global mode to desktop so the card appears as desktop popup
Utils.setMobileMode(false);
},
'click .js-card-zoom-in'(event) {
event.preventDefault();
const current = Utils.getCardZoom();
const newZoom = Math.min(3.0, current + 0.1);
Utils.setCardZoom(newZoom);
},
'click .js-card-zoom-out'(event) {
event.preventDefault();
const current = Utils.getCardZoom();
const newZoom = Math.max(0.5, current - 0.1);
Utils.setCardZoom(newZoom);
},
'click .js-card-mobile-desktop-toggle'(event) {
event.preventDefault();
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'click .js-card-mobile-desktop-toggle'(event) {
event.preventDefault();
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
async 'submit .js-card-description'(event) {
event.preventDefault();
const description = this.currentComponent().getValue();
@ -525,7 +446,7 @@ BlazeComponent.extendComponent({
'click .js-add-members': Popup.open('cardMembers'),
'click .js-assignee': Popup.open('cardAssignee'),
'click .js-add-assignees': Popup.open('cardAssignees'),
'click .js-add-labels': Popup.open('cardLabels'),
'click .js-add-labels'(event) {Popup.open('cardLabels')(event, { dataContextIfCurrentDataIsUndefined: this.currentData() })},
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-start-date': Popup.open('editCardStartDate'),
'click .js-due-date': Popup.open('editCardDueDate'),
@ -534,12 +455,10 @@ BlazeComponent.extendComponent({
'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
'click .js-custom-fields': Popup.open('cardCustomFields'),
'mouseenter .js-card-details'() {
if (this.parentComponent() === null) return;
const parentComponent = this.parentComponent().parentComponent();
//on mobile view parent is Board, not BoardBody.
if (parentComponent === null) return;
parentComponent.showOverlay.set(true);
parentComponent.mouseHasEnterCardDetails = true;
if (this.boardBody) {
this.boardBody.showOverlay.set(true);
this.boardBody.mouseHasEnterCardDetails = true;
}
},
'mousedown .js-card-details'() {
Session.set('cardDetailsIsDragging', false);
@ -560,13 +479,13 @@ BlazeComponent.extendComponent({
'click #toggleCustomFieldsGridButton'() {
Meteor.call('toggleCustomFieldsGrid');
},
'click .js-maximize-card-details'() {
'click .js-maximize-card-details'(e) {
PopupComponent.maximize(e);
Meteor.call('toggleCardMaximized');
autosize($('.card-details'));
},
'click .js-minimize-card-details'() {
'click .js-minimize-card-details'(e) {
PopupComponent.minimize(e);
Meteor.call('toggleCardMaximized');
autosize($('.card-details'));
},
'click .js-vote'(e) {
const forIt = $(e.target).hasClass('js-vote-positive');
@ -737,16 +656,6 @@ Template.cardDetails.helpers({
return uploadProgressManager.getUploadCountForCard(this._id);
}
});
Template.cardDetailsPopup.onDestroyed(() => {
Session.delete('popupCardId');
Session.delete('popupCardBoardId');
});
Template.cardDetailsPopup.helpers({
popupCard() {
const ret = Utils.getPopupCard();
return ret;
},
});
BlazeComponent.extendComponent({
template() {
@ -883,9 +792,7 @@ Template.cardDetailsActionsPopup.events({
'click .js-toggle-watch-card'() {
const currentCard = this;
const level = currentCard.findWatcher(Meteor.userId()) ? null : 'watching';
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
if (!err && ret) Popup.close();
});
Meteor.call('watch', 'card', currentCard._id, level)
},
'click .js-toggle-show-list-on-minicard'() {
const currentCard = this;
@ -896,9 +803,6 @@ Template.cardDetailsActionsPopup.events({
});
BlazeComponent.extendComponent({
onRendered() {
autosize(this.$('textarea.js-edit-card-title'));
},
events() {
return [
{
@ -979,10 +883,6 @@ const filterMembers = (filterTerm) => {
return members;
}
Template.editCardRequesterForm.onRendered(function () {
autosize(this.$('.js-edit-card-requester'));
});
Template.editCardRequesterForm.events({
'keydown .js-edit-card-requester'(event) {
// If enter key was pressed, submit the data
@ -992,10 +892,6 @@ Template.editCardRequesterForm.events({
},
});
Template.editCardAssignerForm.onRendered(function () {
autosize(this.$('.js-edit-card-assigner'));
});
Template.editCardAssignerForm.events({
'keydown .js-edit-card-assigner'(event) {
// If enter key was pressed, submit the data
@ -1469,13 +1365,13 @@ BlazeComponent.extendComponent({
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
@ -1721,13 +1617,13 @@ BlazeComponent.extendComponent({
'DD/MM/YYYY HH:mm',
'DD-MM-YYYY HH:mm'
];
let parsedDate = null;
for (const format of formats) {
parsedDate = parseDate(dateString, [format], true);
if (parsedDate) break;
}
// Fallback to native Date parsing
if (!parsedDate) {
parsedDate = new Date(dateString);
@ -1905,9 +1801,6 @@ EscapeActions.register(
() => {
return !Session.equals('currentCard', null);
},
{
noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
},
);
Template.cardAssigneesPopup.onCreated(function () {
@ -1985,3 +1878,16 @@ Template.cardAssigneePopup.events({
},
'click .js-edit-profile': Popup.open('editProfile'),
});
Template.cardDetailsPopup.helpers({
popupArgs() {
return {
name: "cardDetails",
showHeader: false,
closeDOMs: ["click .js-close-card-details"],
followDOM: ".card-details",
handleDOM: ".card-header-middle",
closeVar: "currentCard"
}
},
});

View file

@ -1,6 +1,6 @@
.card-time {
display: block;
border-radius: 4px;
border-radius: 0.4ch;
padding: 1px 3px;
color: #fff;
background-color: #dbdbdb;

View file

@ -4,7 +4,7 @@
textarea.js-add-checklist-item,
textarea.js-edit-checklist-item {
overflow: hidden;
word-wrap: break-word;
overflow-wrap: break-word;
resize: none;
height: 34px;
}
@ -13,7 +13,7 @@ textarea.js-edit-checklist-item {
.js-convert-checklist-item-to-card {
color: #8c8c8c;
text-decoration: underline;
word-wrap: break-word;
overflow-wrap: break-word;
float: right;
padding-top: 6px;
}
@ -25,6 +25,7 @@ textarea.js-edit-checklist-item {
.checklists-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.checklist-progress-bar-container {
display: flex;
@ -35,7 +36,7 @@ textarea.js-edit-checklist-item {
margin-right: 10px;
}
.checklist-progress-bar-container .checklist-progress-bar {
width: 80%;
flex: 1;
height: 10px;
background-color: #e0e0e0;
border-radius: 16px;
@ -47,19 +48,29 @@ textarea.js-edit-checklist-item {
border-radius: 16px;
height: 100%;
}
.checklist-title {
padding: 10px;
.checklist-controls {
display: flex;
gap: 0.25lh;
}
.checklist-title {
display: flex;
align-items: center;
justify-content: space-between;
}
.checklist-title .checkbox {
float: left;
width: 30px;
height: 30px;
font-size: 18px;
line-height: 30px;
}
.checklist-title .title {
font-size: 18px;
line-height: 25px;
.checklist-title p, .title {
font-size: 1em;
line-height: 1;
margin: 0;
}
.checklist-title .checklist-stat {
margin: 0 0.5em;
@ -79,29 +90,31 @@ textarea.js-edit-checklist-item {
bottom: -600px;
right: 0;
}
.checklist {
background: #f7f7f7;
padding: 0.5lh;
margin: 0.5lh 0;
background-color: #f7f7f7;
}
.checklist.placeholder {
background: #ccc;
border-radius: 2px;
}
.checklist.ui-sortable-helper {
box-shadow: -2px 2px 8px rgba(0,0,0,0.3), 0 0 1px rgba(0,0,0,0.5);
transform: rotate(4deg);
cursor: grabbing;
}
.checklist-item {
margin: 0 0 0 0.1em;
line-height: 18px;
font-size: 1.1em;
margin-top: 3px;
display: flex;
background: #f7f7f7;
gap: 0.25lh;
opacity: 1;
transition: height 0ms 400ms, opacity 400ms 0ms;
height: auto;
overflow: hidden;
align-items: center;
min-height: 1.5lh;
padding: 0 1ch;
}
.checklist-item.is-checked.invisible {
opacity: 0;
@ -114,26 +127,21 @@ textarea.js-edit-checklist-item {
background: #ccc;
border-radius: 2px;
}
.checklist-item.ui-sortable-helper {
box-shadow: -2px 2px 8px rgba(0,0,0,0.3), 0 0 1px rgba(0,0,0,0.5);
transform: rotate(4deg);
cursor: grabbing;
}
.checklist-item:hover {
background-color: #ebebeb;
}
.checklist-item .check-box-container {
padding-right: 10px;
}
.checklist-item .check-box {
margin: 0.1em 0 0 0;
}
.checklist-item .check-box.is-checked {
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
border-bottom: 0.2ch solid #3cb500;
border-right: 0.2ch solid #3cb500;
}
.checklist-item .item-title {
display: flex;
justify-content: start;
flex: 1;
cursor: grab;
}
.checklist-item .item-title.is-checked {
color: #8c8c8c;
@ -141,27 +149,18 @@ textarea.js-edit-checklist-item {
text-decoration: line-through;
}
.checklist-item .item-title .viewer p {
margin-bottom: 2px;
display: block;
word-wrap: break-word;
display: flex;
overflow-wrap: break-word;
max-width: 420px;
}
.checklist-item span.fa.checklistitem-handle {
padding-top: 2px;
padding-right: 10px;
}
.js-delete-checklist-item,
.js-convert-checklist-item-to-card {
margin: 0 0 0.5em 1.33em;
padding: 12px 0 0 0;
}
.add-checklist-item {
margin: 0.2em 0 0.5em 1.33em;
}
.add-checklist-item.js-open-inlined-form,
.add-checklist.js-open-inlined-form {
display: block;
width: 50%;
display: inline-block;
}
.add-checklist-item.js-open-inlined-form:hover,
.add-checklist.js-open-inlined-form:hover {
@ -169,25 +168,13 @@ textarea.js-edit-checklist-item {
color: #222;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
.add-checklist-top {
/* more space to checklists title */
padding-left: 20px;
/* + is easier clickable */
padding-right: 20px;
}
.add-checklist-top.js-open-inlined-form:hover {
background: #dbdbdb;
color: #222;
box-shadow: 0 1px 2px rgba(0,0,0,.2);
}
.card-details-item-title {
/* max width for adding checklist at top */
width: 100%;
}
.checklist-details-menu {
float: right;
padding: 6px 10px 6px 10px;
}
.edit-controls label.toggle-label {
margin-left: 2px;
}

View file

@ -36,26 +36,31 @@ template(name="checklistDetail")
+editChecklistItemForm(checklist = checklist)
else
.checklist-title
span
if canModifyCard
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
if canModifyCard
h4.title.js-open-inlined-form.is-editable
if isTouchScreenOrShowDesktopDragHandles
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
h4.title
if canModifyCard
a.js-open-inlined-form.is-editable(title="{{_ 'moveChecklistPopup-title'}}")
+viewer
= checklist.title
else
+viewer
= checklist.title
else
h4.title
+viewer
.checklist-controls
if canModifyCard
a.fa.fa-navicon.checklist-details-menu.js-open-checklist-details-menu(title="{{_ 'checklistActionsPopup-title'}}")
if isMiniScreen
span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
+viewer
= checklist.title
if $gt finishedPercent 0
.checklist-progress-bar-container
.checklist-progress-text {{finishedPercent}}%
.checklist-progress-bar
//- jumps where checking the first item is not comfortable;
//- so try to show it anytime. also, it helps to separate the checklists.
.checklist-progress-bar-container
.checklist-progress-text {{finishedPercent}}%
.checklist-progress-bar
if $gt finishedPercent 0
.checklist-progress(style="width:{{finishedPercent}}%")
else
.checklist-progress(style="visibility:hidden")
+checklistItems(checklist = checklist card = card)
template(name="checklistDeletePopup")
@ -64,7 +69,7 @@ template(name="checklistDeletePopup")
template(name="addChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
span.copied-tooltip.copied-tooltip-hidden {{_ 'copied'}}
textarea.js-add-checklist-item(rows='1' autofocus)
.edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
@ -73,16 +78,12 @@ template(name="addChecklistItemForm")
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItem'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItem")
label.toggle-label(for="toggleNewlineBecomesNewChecklistItem")
span.toggle-switch-desc
| {{_ 'newLineNewItem'}}
if $eq position 'top'
.material-toggle-switch(title="{{_ 'newlineBecomesNewChecklistItemOriginOrder'}}")
input.toggle-switch(type="checkbox" id="toggleNewlineBecomesNewChecklistItemOriginOrder")
label.toggle-label(for="toggleNewlineBecomesNewChecklistItemOriginOrder")
| {{_ 'originOrder'}}
template(name="editChecklistItemForm")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
span.copied-tooltip.copied-tooltip-hidden {{_ 'copied'}}
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item'
= item.title
@ -99,13 +100,6 @@ template(name="editChecklistItemForm")
| {{_ 'convertChecklistItemToCardPopup-title'}}
template(name="checklistItems")
if checklist.items.length
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist position="top")
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
.checklist-items.js-checklist-items
each item in checklist.items
+inlinedForm(classNames="js-edit-checklist-item" item = item checklist = checklist)
@ -118,14 +112,15 @@ template(name="checklistItems")
else
a.add-checklist-item.js-open-inlined-form(title="{{_ 'add-checklist-item'}}")
i.fa.fa-plus
+inlinedForm(autoclose=false classNames="js-add-checklist-item" checklist = checklist)
+addChecklistItemForm(checklist=checklist showNewlineBecomesNewChecklistItem=true position="top")
template(name='checklistItemDetail')
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}"
role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
.js-checklist-item.checklist-item(class="{{#if item.isFinished }}is-checked{{#if checklist.hideCheckedChecklistItems}} invisible{{/if}}{{/if}}{{#if checklist.hideAllChecklistItems}} is-checked invisible{{/if}}" role="checkbox" aria-checked="{{#if item.isFinished }}true{{else}}false{{/if}}" tabindex="0")
if canModifyCard
.check-box-container
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
if isTouchScreenOrShowDesktopDragHandles
if isMiniScreen
span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
+viewer
@ -141,16 +136,16 @@ template(name="checklistActionsPopup")
li
a.js-delete-checklist.delete-checklist
i.fa.fa-trash
| {{_ "delete"}} ...
| {{_ "delete"}}
a.js-move-checklist.move-checklist
i.fa.fa-arrow-right
| {{_ "moveChecklist"}} ...
| {{_ "moveChecklist"}}
a.js-copy-checklist.copy-checklist
i.fa.fa-copy
| {{_ "copyChecklist"}} ...
| {{_ "copyChecklist"}}
a.js-hide-checked-checklist-items
i.fa.fa-eye-slash
| {{_ "hideCheckedChecklistItems"}} ...
| {{_ "hideCheckedChecklistItems"}}
.material-toggle-switch(title="{{_ 'hide-checked-items'}}")
if checklist.hideCheckedChecklistItems
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}" checked="checked")
@ -158,7 +153,7 @@ template(name="checklistActionsPopup")
input.toggle-switch(type="checkbox" id="toggleHideCheckedChecklistItems_{{checklist._id}}")
label.toggle-label(for="toggleHideCheckedChecklistItems_{{checklist._id}}")
a.js-hide-all-checklist-items
i.fa.fa-ban
| 🚫
| {{_ "hideAllChecklistItems"}} ...
.material-toggle-switch(title="{{_ 'hideAllChecklistItems'}}")
if checklist.hideAllChecklistItems

View file

@ -7,7 +7,7 @@ import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwim
const subManager = new SubsManager();
const { calculateIndexData, capitalize } = Utils;
function initSorting(items) {
function initSorting(items, handleSelector) {
items.sortable({
tolerance: 'pointer',
helper: 'clone',
@ -16,6 +16,7 @@ function initSorting(items) {
appendTo: 'parent',
distance: 7,
placeholder: 'checklist-item placeholder',
handle: handleSelector,
scroll: true,
start(evt, ui) {
ui.placeholder.height(ui.helper.height());
@ -48,8 +49,9 @@ function initSorting(items) {
BlazeComponent.extendComponent({
onRendered() {
const self = this;
this.handleSelector = Utils.isMiniScreen() ? 'span.fa.checklistitem-handle' : '.item-title';
self.itemsDom = this.$('.js-checklist-items');
initSorting(self.itemsDom);
initSorting(self.itemsDom, this.handleSelector);
self.itemsDom.mousedown(function (evt) {
evt.stopPropagation();
});
@ -63,11 +65,9 @@ BlazeComponent.extendComponent({
const $itemsDom = $(self.itemsDom);
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$(self.itemsDom).sortable({
handle: 'span.fa.checklistitem-handle',
});
}
$(self.itemsDom).sortable({
handle: this.handleSelector,
});
}
});
},

View file

@ -1,20 +1,20 @@
.card-label {
border: 1px solid #000;
border-radius: 4px;
border-radius: 0.4ch;
color: #fff;
display: inline-block;
font-weight: 700;
font-size: 13px;
margin-right: 4px;
margin-bottom: 5px;
padding: 3px 8px;
max-width: 210px;
min-width: 8px;
word-wrap: break-word;
min-height: 18px;
vertical-align: middle;
white-space: initial;
overflow: initial;
font-size: 0.9em;
display: flex;
/* prefer not using padding/margin but let outer grids
position/size labels (see e.g. minicards), otherwise we get
inconsistencies */
align-self: stretch;
justify-content: center;
align-items: center;
text-align: center;
padding: 0 0.5ch;
height: var(--label-height);
min-width: 8ch;
}
.card-label:hover {
color: #fff;
@ -34,6 +34,7 @@
}
.card-label p {
margin: 0px;
--overflow-lines: 1;
}
.palette-colors {
display: flex;
@ -138,37 +139,22 @@
.card-label-indigo {
background-color: #4b0082;
}
.edit-label .card-label,
.create-label .card-label {
float: left;
height: 25px;
margin: 0px 3% 7px 0px;
width: 10.5%;
max-width: 10.5%;
cursor: pointer;
}
.edit-labels input[type="text"] {
margin: 4px 0 6px 38px;
width: 243px;
}
.edit-labels .card-label {
height: 30px;
left: 0;
padding: 1px 5px;
position: absolute;
top: 0;
width: 24px;
}
.edit-labels .labels-static .card-label {
line-height: 30px;
margin-bottom: 4px;
position: relative;
top: auto;
left: 0;
width: 260px;
}
.edit-labels-pop-over {
margin-bottom: 8px;
display: grid;
/* so that inner elements, align nicely */
grid-template-columns: 1fr;
gap: 0.1lh;
>li {
display: flex;
flex-direction: row-reverse;
gap: 1ch;
align-items: center;
}
.card-label-selectable {
flex: 1;
display: flex;
gap: 1ch;
}
}
.edit-labels-pop-over .card-label .viewer p {
margin: 0;
@ -176,34 +162,6 @@
.edit-labels-pop-over .shortcut {
display: inline-block;
}
.card-label-selectable {
border-radius: 3px;
cursor: pointer;
margin: 0;
margin-bottom: 3px;
width: 190px;
min-height: 18px;
padding: 8px;
position: relative;
transition: margin-right 0.1s;
}
.card-label-selectable .card-label-selectable-icon {
position: absolute;
top: 8px;
right: -20px;
}
.card-label-selectable.active:hover,
.card-label-selectable.active,
.card-label-selectable.active.selected:hover,
.card-label-selectable.active.selected {
padding-right: 32px;
}
.card-label-selectable.active:hover .card-label-selectable-icon,
.card-label-selectable.active .card-label-selectable-icon,
.card-label-selectable.active.selected:hover .card-label-selectable-icon,
.card-label-selectable.active.selected .card-label-selectable-icon {
right: 6px;
}
.card-label-selectable.selected,
.card-label-selectable:hover {
opacity: 0.8;
@ -212,24 +170,6 @@
.active .card-label-selectable:hover {
margin-right: 0;
}
.active .card-label-selectable .card-label-selectable-icon {
right: 8px;
}
.card-label-edit-button {
border-radius: 3px;
float: right;
padding: 8px;
}
.card-label-edit-button:hover {
background: #dbdbdb;
}
ul.edit-labels-pop-over span.label-handle {
padding-right: 10px;
display: inline-block;
width: 1.2em;
text-align: center;
color: #999;
}
ul.edit-labels-pop-over span.label-handle + .card-label {
max-width: 180px;
}
}

View file

@ -1,10 +1,32 @@
.minicard-body {
display: flex;
flex-direction: column;
padding: 0 1ch 0.2lh 1ch;
gap: 0.2lh;
}
.minicard-wrapper {
cursor: pointer;
position: relative;
display: flex;
align-items: center;
margin-bottom: 1.2vh;
height: min-content;
}
.minicard-header {
display: flex;
align-items: center;
padding: 0 1ch;
gap: 1ch;
}
.minicard > hr {
margin: 0;
}
.minicard-add-form {
width: auto;
}
.minicard-wrapper.placeholder {
background: #ccc;
border-radius: 1.2vw;
@ -28,32 +50,25 @@
.minicard-wrapper .multi-selection-checkbox + .minicard {
margin-left: 1vw;
}
@media only screen {
.minicard {
padding: 0.8vh 1vw 0.3vh;
position: relative;
flex: 1;
flex-wrap: wrap;
background-color: #fff;
min-height: 2.5vh;
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
border-radius: 0.3vw;
color: #4d4d4d;
overflow: hidden;
transition: transform 0.2s, border-radius 0.2s;
}
.minicard {
display: grid;
grid-auto-flow: row;
grid-template-rows: min-content 1fr auto auto;
gap: 0.4lh;
background-color: #fff;
box-shadow: 0 0.2vh 0.3vh rgba(0,0,0,0.15);
border-radius: 0.3vw;
color: #4d4d4d;
overflow: hidden;
transition: transform 0.2s, border-radius 0.2s;
flex: 1;
}
.minicard-details-menu-with-handle {
float: right;
padding-left: 0.7vw;
font-size: clamp(14px, 3vw, 18px);
padding: 0;
z-index: 1;
}
.minicard-details-menu {
float: right;
font-size: clamp(14px, 3vw, 18px);
padding-left: 0.7vw;
.minicard-actions-right {
justify-content: end;
display: flex;
align-items: end;
gap: .5lh;
}
@media print {
.minicard-details-menu,
@ -76,7 +91,6 @@
transform: translateX(1.5vw);
border-bottom-right-radius: 0;
border-top-right-radius: 0;
z-index: 25;
box-shadow: -0.3vw 0.2vh 0.3vh rgba(0,0,0,0.2);
}
.minicard:hover:not(.minicard-composer),
@ -96,20 +110,30 @@
margin: 0.8vh -1vw 0.8vh -1vw;
border-radius: top 0.3vw;
}
.minicard .minicard-labels {
float: none;
margin-right: 6vw;
}
.minicard .minicard-labels .minicard-label {
width: clamp(12px, 1.5vw, 16px);
height: clamp(12px, 1.5vw, 16px);
border-radius: 0.3vw;
margin-right: 0.4vw;
margin-bottom: 0.4vh;
}
.minicard .minicard-labels-no-text {
display: flex;
flex-wrap: wrap;
.minicard {
.minicard-labels, .dates {
display: grid;
grid-auto-rows: min-content;
justify-content: stretch;
font-size: 0.8em;
grid-auto-rows: minmax(1.3lh, auto);
}
.minicard-labels {
grid-template-columns: repeat(auto-fill, minmax(12ch, auto));
gap: 0.2lh 0.5ch;
}
.minicard-labels-no-text {
grid-template-columns: repeat(auto-fill, 4ch);
grid-template-rows: 4ch;
font-size: 0.4em;
.minicard-label {
border-radius: 1ch;
}
}
.dates {
height: min-content;
grid-template-columns: repeat(auto-fit, minmax(15ch, auto));
}
}
.minicard .minicard-custom-fields {
display: block;
@ -121,26 +145,22 @@
.minicard .minicard-custom-field-item {
flex-grow: 1;
display: block;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 13vw;
margin-right: 0.5vw;
}
.minicard .minicard-custom-field-item-fullwidth {
flex-grow: 1;
display: block;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
margin-right: 0.5vw;
}
.minicard .handle {
width: clamp(20px, 2.5vw, 28px);
height: clamp(20px, 2.5vw, 28px);
position: absolute;
right: 0vw;
top: 4vh;
display: none;
z-index: 1;
}
@media only screen {
.minicard .handle {
display: block;
@ -154,58 +174,34 @@
text-align: center;
}
.minicard .minicard-title {
margin-right: 1.5vw;
display: flex;
max-width: 100%;
flex: 1;
cursor: grab;
.viewer {
--overflow-lines: 2;
}
}
.minicard .minicard-title .card-number {
color: #b3b3b3;
display: inline-block;
margin-right: 0.7vw;
}
@media only screen {
.minicard .minicard-title p:last-child {
margin-bottom: 0;
}
.minicard .minicard-title .viewer {
display: block;
word-wrap: break-word;
}
}
.minicard .dates {
display: flex;
flex-direction: row;
flex-wrap: wrap;
position: relative;
z-index: 5;
margin-right: 6vw;
clear: both;
}
.minicard .date {
margin-right: 0.4vw;
display: flex;
&>a {
display: flex;
justify-content: center;
flex: 1;
align-items: center;
text-align: center;
align-self: stretch;
}
}
/* Unicode icons for minicard dates - matching cardDate.css */
.minicard .card-date.end-date time::before {
content: "🏁"; /* Finish flag - represents end/completion */
}
.minicard .card-date.due-date time::before {
content: "⏰"; /* Alarm clock - represents due/deadline */
}
.minicard .card-date.start-date time::before {
content: "🚀"; /* Rocket - represents start/launch */
}
.minicard .card-date.received-date time::before {
content: "📥"; /* Inbox tray - represents received/incoming */
}
.minicard .card-date time::before {
font-size: inherit;
margin-right: 0.3em;
display: inline-block;
}
/* Date type specific colors for minicards - matching cardDate.css */
.minicard .card-date.received-date {
background-color: #dbdbdb; /* Grey for received - same as base card-date */
background-color: #d3d3d3; /* Grey for received - a bit darker than base card-date */
}
.minicard .card-date.received-date:hover,
@ -311,102 +307,134 @@
background-color: #1976d2 !important;
}
.minicard .minicard-badges-and-creator {
display: flex;
flex-direction: row-reverse;
justify-content: end;
gap: 0 0.5ch;;
}
.minicard-people-grid {
display: grid;
grid-template-columns: 1fr auto;
grid-auto-rows: auto;
}
.minicard-people-wrapper {
display: flex;
justify-content: end;
gap: 0.1lh;
}
.minicard .badges {
float: left;
margin-top: 1vh;
display: flex;
align-items: center;
gap: 1.5ch;
color: #808080;
/* this avoid padding-ish at the bottom of the card */
font-size: 0.8rem;
}
.minicard .badges:empty {
display: none;
}
.minicard .badges .badge {
float: left;
margin-right: 1.5vw;
margin-bottom: 0.4vh;
font-size: 0.9em;
}
.minicard .badges .badge.is-finished {
background: #3cb500;
padding: 0 0.4vw;
padding: 0.3lh 0.8ch;
border-radius: 0.4vw;
color: #fff;
&, .fa {
color: #fff;
}
}
.minicard .badges .badge:last-of-type {
margin-right: 0;
}
.minicard .badges .badge .badge-icon,
.minicard .badges .badge .badge-text {
vertical-align: middle;
}
.minicard .badges .badge .badge-icon.badge-comment,
.minicard .badges .badge .badge-text.badge-comment {
margin-bottom: 0.1rem;
}
.minicard .badges .badge .badge-text {
font-size: 0.9em;
padding-left: 0.3vw;
line-height: 1.2;
}
.minicard .badges .badge .check-list-text {
padding-left: 0px;
line-height: 1.1;
}
.minicard .minicard-members,
.minicard .minicard-assignees,
.minicard .minicard-creator {
float: right;
margin-left: 0.7vw;
margin-bottom: 0.5vh;
}
.minicard .minicard-members .member,
.minicard .minicard-assignees .member,
.minicard .minicard-creator .member {
float: right;
display: flex;
border-radius: 50%;
height: clamp(24px, 3.5vw, 32px);
width: clamp(24px, 3.5vw, 32px);
margin-bottom: 0.5vh;
font-size: 0.8em;
margin-bottom: 0.2lh;
}
.minicard .minicard-members .assignee,
.minicard .minicard-assignees .assignee,
.minicard .minicard-creator .assignee {
float: right;
border-radius: 50%;
height: clamp(24px, 3.5vw, 32px);
width: clamp(24px, 3.5vw, 32px);
.minicard .minicard-assignees .member {
border: 2px solid rgb(180, 87, 87);
}
.minicard .minicard-members + .badges,
.minicard .minicard-assignees + .badges,
.minicard .minicard-creator + .badges {
margin-top: 0.7vh;
.minicard .minicard-creator .member {
border: 2px solid #7fd67f;
}
.minicard .minicard-members .member {
border: 2px solid #5a5ac6;
}
.minicard .minicard-assignees {
border-bottom: 1px solid #f00;
}
.minicard .minicard-creator {
border-bottom: 1px solid #008000;
display: flex;
}
.minicard .minicard-members:empty,
.minicard .minicard-assignees:empty {
display: none;
}
.minicard .minicard-description {
padding: 0.8vh 0 0 1vw;
color: #000;
background-color: #eee;
width: 100%;
margin-bottom: 0.3vh;
margin-left: -0.5vw;
border-radius: 0.4vw;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
padding: 0.5lh 1ch;
--overflow-lines: 2;
.viewer {
font-size: 0.9em;
ul {
padding-bottom: 0;
}
}
}
.minicard .minicard-description p {
margin: 0;
}
.minicard-composer {
display: flex;
flex-direction: column;
.minicard-composer-icons {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2lh;
}
.minicard-bottom {
display: flex;
justify-content: end;
align-items: center;
gap: 1ch;
.minicard-composer-icons {
display: flex;
flex: 1;
flex-wrap: wrap;
flex-direction: row-reverse;
}
.add-controls {
display: flex;
align-self: start;
}
textarea {
display: flex;
flex: 1;
}
}
}
.minicard.minicard-composer {
margin-bottom: 1.3vh;
flex-wrap: wrap;
flex: 1;
align-self: stretch;
gap: 0.3lh;
padding: 0.5lh 1ch;
position: relative;
}
.minicard.minicard-composer textarea.minicard-composer-textarea,
.minicard.minicard-composer textarea.minicard-composer-textarea:focus {
resize: none;
@ -415,11 +443,11 @@
box-shadow: none;
height: auto;
margin: 0;
padding: 0;
max-height: 22vh;
min-height: 5vh;
margin-bottom: 2.5vh;
padding: 1ch;
min-height: 5lh;
overflow-y: auto;
flex: 1;
width: 100%;
}
.parent-prefix {
color: #b3b3b3;
@ -734,30 +762,12 @@
/* List name display on minicard */
.minicard-list-name {
font-size: 0.75em;
font-size: inherit;
color: #8c8c8c;
margin-top: 0.2vh;
display: flex;
align-items: center;
gap: 0.3vw;
}
/* Checklist display on minicard */
.minicard-checklist {
width: 100%;
margin-top: 0.5vh;
margin-bottom: 0.5vh;
padding: 0.3vh 0.5vw;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0.3vw;
border: 1px solid #e0e0e0;
}
.minicard-checklist .checklist-header {
padding: 0 0.5ch;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.3vh;
gap: 0.5ch;
}
.minicard-checklist .checklist-title {

View file

@ -1,226 +1,236 @@
template(name="minicard")
if isSelected
+cardDetailsPopup(this)
.minicard.nodragscroll(
class="{{#if isLinkedCard}}linked-card{{/if}}"
class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="{{#if colorClass}}minicard-{{colorClass}}{{/if}}")
if canMoveCard
if isTouchScreenOrShowDesktopDragHandles
.handle
i.fa.fa-arrows
if canModifyCard
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
.dates
if getReceived
.date
+minicardReceivedDate
if getStart
.date
+minicardStartDate
if getDue
.date
+minicardDueDate
if getEnd
+minicardEndDate
if allowsReceivedDate
if getReceived
.date.viewer
+minicardReceivedDate
if allowsStartDate
if getStart
.date.viewer
+minicardStartDate
if allowsDueDate
if getDue
.date.viewer
+minicardDueDate
if allowsEndDate
if getEnd
.date.viewer
+minicardEndDate
if getSpentTime
.date
.date.viewer
+cardSpentTime
if cover
if currentBoard.allowsCoverAttachmentOnMinicard
.minicard-cover(style="background-image: url('{{cover.link 'original'}}?dummyReloadAfterSessionEstablished={{sess}}');")
.minicard-header
.minicard-title
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
.parent-prefix
| {{ parentString ' > ' }}
if $eq 'prefix-with-parent' currentBoard.presentParentTask
.parent-prefix
| {{ parentCardName }}
if isLinkedBoard
a.js-linked-link
span.linked-icon
i.fa.fa-folder
else if isLinkedCard
a.js-linked-link
span.linked-icon
i.fa.fa-id-card
if getArchived
span.linked-icon.linked-archived
i.fa.fa-archive
+viewer
if allowsCardNumber
span.card-number
| ##{getCardNumber}&thinsp;
= getTitle
div.minicard-actions-right
if canModifyCard
a.minicard-details-menu-with-handle.js-open-minicard-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
if isMiniScreen
if canMoveCard
.handle
i.fa.fa-arrows
hr
.minicard-body
if cover
if allowsCoverAttachmentOnMinicard
.minicard-cover(style="background-image: url('{{cover.link 'original'}}?dummyReloadAfterSessionEstablished={{sess}}');")
// Upload progress indicator for drag-and-drop uploads
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}}")
.upload-progress-filename {{file.name}}
.upload-progress-bar
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-warning
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
span {{_ 'upload-completed'}}
//- Upload progress indicator for drag-and-drop uploads
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}}")
.upload-progress-filename {{file.name}}
.upload-progress-bar
.upload-progress-fill(style="width: {{progress}}%")
if $eq status 'error'
.upload-progress-error
i.fa.fa-warning
span {{_ 'upload-failed'}}
else if $eq status 'completed'
.upload-progress-success
i.fa.fa-check
span {{_ 'upload-completed'}}
.minicard-title
if $eq 'prefix-with-full-path' currentBoard.presentParentTask
.parent-prefix
| {{ parentString ' > ' }}
if $eq 'prefix-with-parent' currentBoard.presentParentTask
.parent-prefix
| {{ parentCardName }}
if isLinkedBoard
a.js-linked-link
span.linked-icon
i.fa.fa-folder
else if isLinkedCard
a.js-linked-link
span.linked-icon
i.fa.fa-id-card
if getArchived
span.linked-icon.linked-archived
i.fa.fa-archive
+viewer
if currentBoard.allowsCardNumber
span.card-number
| ##{getCardNumber}
= getTitle
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
unless hiddenMinicardLabelText
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if hiddenMinicardLabelText
.minicard-label(class="card-label-{{color}}" title="{{name}}")
if labels
.minicard-labels(class="{{#if hiddenMinicardLabelText}}minicard-labels-no-text{{/if}}")
each labels
unless hiddenMinicardLabelText
span.js-card-label.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if hiddenMinicardLabelText
.minicard-label(class="card-label-{{color}}" title="{{name}}")
.minicard-custom-fields
each customFieldsWD
if definition.showOnCard
if trueValue
.minicard-custom-field
// If there is custom field label, show label at left,
// and value at right
if definition.showLabelOnMiniCard
.minicard-custom-field-item
+viewer
= definition.name
.minicard-custom-field-item
if $eq definition.type "currency"
.minicard-custom-fields
each customFieldsWD
if definition.showOnCard
if trueValue
.minicard-custom-field
// If there is custom field label, show label at left,
// and value at right
if definition.showLabelOnMiniCard
.minicard-custom-field-item
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
.materialCheckBox(class="{{#if value }}is-checked{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
else
// If there is no custom field label,
// show value full width
.minicard-custom-field-item-fullwidth
if $eq definition.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
.materialCheckBox(class="{{#if value }}is-checked{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
= definition.name
.minicard-custom-field-item
if $eq definition.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
.materialCheckBox(class="{{#if value }}is-checked{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
else
// If there is no custom field label,
// show value full width
.minicard-custom-field-item-fullwidth
if $eq definition.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(definition)
else if $eq definition.type "date"
.date
+minicardCustomFieldDate
else if $eq definition.type "checkbox"
.materialCheckBox(class="{{#if value }}is-checked{{/if}}")
else if $eq definition.type "stringtemplate"
+viewer
= formattedStringtemplateCustomFieldValue(definition)
else
+viewer
= trueValue
.minicard-people-grid
if allowsAssignee
if getAssignees
.minicard-people-wrapper
.minicard-assignees.js-minicard-assignees
each getAssignees
+userAvatar(userId=this)
if showAssignee
if getAssignees
.minicard-assignees.js-minicard-assignees
each getAssignees
+userAvatar(userId=this)
if allowsMembers
if getMembers
.minicard-people-wrapper
.minicard-members.js-minicard-members
each getMembers
+userAvatar(userId=this)
if showMembers
if getMembers
.minicard-members.js-minicard-members
each getMembers
+userAvatar(userId=this)
.minicard-badges-and-creator
if allowsCreatorOnMinicard
.minicard-creator
+userAvatar(userId=this.userId noRemove=true)
if showCreatorOnMinicard
.minicard-creator
+userAvatar(userId=this.userId noRemove=true)
.badges
if canModifyCard
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text
i.fa.fa-comment-o
= ' '
= comments.length
//span.badge-comment.badge-text
//| {{_ 'comment'}}
if getDescription
unless currentBoard.allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription)
span.badge-icon
i.fa.fa-file-text-o
if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon(class="{{#if voteState}}text-green{{/if}}")
i.fa.fa-thumbs-up
span.badge-text {{ voteCountPositive }}
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}")
i.fa.fa-thumbs-down
span.badge-text {{ voteCountNegative }}
if getPokerQuestion
.badge.badge-state-image-only(title=getPokerQuestion)
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}")
i.fa.fa-check-square
if expiredPoker
span.badge-text {{ getPokerEstimation }}
if attachments.length
if currentBoard.allowsBadgeAttachmentOnMinicard
.badge
span.badge-icon
i.fa.fa-paperclip
span.badge-text= attachments.length
if allSubtasks.count
.badge
span.badge-icon
i.fa.fa-globe
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
i.fa.fa-sort-numeric-asc
span.badge-text.check-list-sort {{ sort }}
if shouldShowChecklistAtMinicard
each shouldShowChecklistAtMinicard
+minicardChecklist(checklist=. card=..)
if currentBoard.allowsDescriptionTextOnMinicard
.badges
if canModifyCard
if allowsComments
if comments.length
.badge(title="{{_ 'card-comments-title' comments.length }}")
span.badge-icon.badge-comment.badge-text
i.fa.fa-comment-o
= ' '
= comments.length
//span.badge-comment.badge-text
//| {{_ 'comment'}}
if getDescription
unless allowsDescriptionTextOnMinicard
.badge.badge-state-image-only(title=getDescription)
span.badge-icon
i.fa.fa-file-text-o
if getVoteQuestion
.badge.badge-state-image-only(title=getVoteQuestion)
span.badge-icon(class="{{#if voteState}}text-green{{/if}}")
i.fa.fa-thumbs-up
span.badge-text {{ voteCountPositive }}
span.badge-icon(class="{{#if $eq voteState false}}text-red{{/if}}")
i.fa.fa-thumbs-down
span.badge-text {{ voteCountNegative }}
if getPokerQuestion
.badge.badge-state-image-only(title=getPokerQuestion)
span.badge-icon(class="{{#if pokerState}}text-green{{/if}}")
i.fa.fa-check-square
if expiredPoker
span.badge-text {{ getPokerEstimation }}
if attachments.length
if allowsBadgeAttachmentOnMinicard
.badge
span.badge-icon
i.fa.fa-paperclip
span.badge-text= attachments.length
if checklists.length
if allowsChecklists
.badge(class="{{#if checklistFinished}}is-finished{{/if}}")
span.badge-icon
i.fa.fa-check
span.badge-text.check-list-text {{checklistFinishedCount}}/{{checklistItemCount}}
if allSubtasks.count
if allowsSubtasks
.badge
span.badge-icon
i.fa.fa-globe
span.badge-text.check-list-text {{subtasksFinishedCount}}/{{allSubtasksCount}}
//{{subtasksFinishedCount}}/{{subtasksCount}} does not work because when a subtaks is archived, the count goes down
if allowsCardSortingByNumber
if allowsCardSortingByNumberOnMinicard
.badge
span.badge-icon
i.fa.fa-sort-numeric-asc
span.badge-text.check-list-sort {{ sort }}
if shouldShowListOnMinicard
.minicard-list-name
span
i.fa.fa-list
span
| {{ listName }}
if $eq 'subtext-with-full-path' presentParentTask
.parent-subtext
| {{ parentString ' > ' }}
if $eq 'subtext-with-parent' presentParentTask
.parent-subtext
| {{ parentCardName }}
if allowsDescriptionTextOnMinicard
if getDescription
.minicard-description
+viewer
| {{ getDescription }}
if shouldShowListOnMinicard
.minicard-list-name
i.fa.fa-list
| {{ listName }}
if $eq 'subtext-with-full-path' currentBoard.presentParentTask
.parent-subtext
| {{ parentString ' > ' }}
if $eq 'subtext-with-parent' currentBoard.presentParentTask
.parent-subtext
| {{ parentCardName }}
template(name="editCardSortOrderPopup")
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
input.js-edit-card-sort-popup(type='text' value=sort dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
template(name="minicardChecklist")
.minicard-checklist
.checklist-header
.checklist-title= checklist.title
if canModifyCard
a.checklist-menu.js-open-checklist-menu(title="{{_ 'checklistActionsPopup-title'}}")
i.fa.fa-bars
each visibleItems
+checklistItemDetail(item = . checklist = checklist card = card)
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}

View file

@ -13,6 +13,24 @@ BlazeComponent.extendComponent({
return 'minicard';
},
onRendered() {
// cannot be done with CSS because newlines
// rendered by the JADE engine count as non empty
// and some "empty" divs are nested
// this is not very robust and could probably be
// done with a helper, but it could be in fact worse
// because we would need to to if (allowsX() && X() && ...)
const body = $(this.find('.minicard-body'));
if (!body) {return}
let emptyChildren;
do {
emptyChildren = body.find('*').filter((_, e) => !e.classList.contains('fa') && $(e).html().trim().length === 0).remove();
} while (emptyChildren.length > 0)
if (body.html().trim().length === 0) {
body.parent().find('hr:has(+ .minicard-body)').remove();
}
},
formattedCurrencyCustomFieldValue(definition) {
const customField = this.data()
.customFieldsWD()
@ -39,46 +57,14 @@ BlazeComponent.extendComponent({
return ret;
},
showCreatorOnMinicard() {
// cache "board" to reduce the mini-mongodb access
const board = this.data().board();
let ret = false;
if (board) {
ret = board.allowsCreatorOnMinicard ?? false;
}
return ret;
},
isWatching() {
const card = this.currentData();
return card.findWatcher(Meteor.userId());
},
showMembers() {
// cache "board" to reduce the mini-mongodb access
const board = this.data().board();
let ret = false;
if (board) {
ret =
board.allowsMembers === null ||
board.allowsMembers === undefined ||
board.allowsMembers
;
}
return ret;
},
showAssignee() {
// cache "board" to reduce the mini-mongodb access
const board = this.data().board();
let ret = false;
if (board) {
ret =
board.allowsAssignee === null ||
board.allowsAssignee === undefined ||
board.allowsAssignee
;
}
return ret;
isSelected() {
const card = this.currentData();
return Session.get('currentCard') === card._id;
},
/** opens the card label popup only if clicked onto a label
@ -87,6 +73,8 @@ BlazeComponent.extendComponent({
*/
cardLabelsPopup(event) {
if (this.find('.js-card-label:hover')) {
event.preventDefault();
event.stopPropagation();
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
}
},
@ -203,7 +191,7 @@ BlazeComponent.extendComponent({
visibleItems() {
const checklist = this.currentData().checklist || this.currentData();
const items = checklist.items();
return items.filter(item => {
// Hide finished items if hideCheckedChecklistItems is true
if (item.isFinished && checklist.hideCheckedChecklistItems) {
@ -254,33 +242,8 @@ Template.minicard.helpers({
},
shouldShowListOnMinicard() {
// Show list name if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific card has the setting enabled
const currentBoard = this.board();
if (!currentBoard) return false;
return currentBoard.allowsShowListsOnMinicard || this.showListOnMinicard;
return Utils.allowsShowLists();
},
shouldShowChecklistAtMinicard() {
// Return checklists that should be shown on minicard
const currentBoard = this.board();
if (!currentBoard) return [];
const checklists = this.checklists();
const visibleChecklists = [];
checklists.forEach(checklist => {
// Show checklist if either:
// 1. Board-wide setting is enabled, OR
// 2. This specific checklist has the setting enabled
if (currentBoard.allowsChecklistAtMinicard || checklist.showChecklistAtMinicard) {
visibleChecklists.push(checklist);
}
});
return visibleChecklists;
}
});
BlazeComponent.extendComponent({

View file

@ -18,7 +18,8 @@
font-weight: bold;
}
.result-card-context-list {
margin-bottom: 0.7rem;
display: flex;
gap: 0.2ch;
}
.result-card-block-wrapper {
display: inline-block;

View file

@ -14,17 +14,10 @@ BlazeComponent.extendComponent({
onReady() {
Session.set('popupCardId', cardId);
Session.set('popupCardBoardId', boardId);
this_.cardDetailsPopup(evt);
},
});
},
cardDetailsPopup(event) {
if (!Popup.isOpen()) {
Popup.open("cardDetails")(event);
}
},
events() {
return [
{

View file

@ -4,19 +4,14 @@
textarea.js-add-subtask-item,
textarea.js-edit-subtask-item {
overflow: hidden;
word-wrap: break-word;
overflow-wrap: break-word;
resize: none;
height: 34px;
}
.delete-text,
.subtask-title .js-delete-subtask,
.subtask-title .js-view-subtask,
.js-delete-subtask-item {
color: #8c8c8c;
text-decoration: underline;
word-wrap: break-word;
float: right;
padding-top: 6px;
}
.delete-text:hover,
.subtask-title .js-delete-subtask:hover,
@ -28,11 +23,11 @@ textarea.js-edit-subtask-item {
float: left;
width: 30px;
height: 30px;
font-size: 18px;
line-height: 30px;
}
.subtask-title .title {
font-size: 18px;
line-height: 25px;
}
.subtask-title .subtasks-stat {
@ -133,7 +128,7 @@ textarea.js-edit-subtask-item {
margin: 0.1em 0 0 0;
}
.subtasks-item .check-box.is-checked {
border-bottom: 2px solid #3cb500;
border-bottom: 0.2ch solid #3cb500;
border-right: 2px solid #3cb500;
}
/* Unicode checkbox icons styling */
@ -165,16 +160,4 @@ body.grey-icons-enabled .subtasks-item .check-box-unicode {
}
.subtasks-item .item-title .viewer p {
margin-bottom: 2px;
}
.js-delete-subtask-item {
margin: 0 0 0.5em 1.33em;
padding: 12px 0 0 0;
}
.add-subtask-item {
margin: 0.2em 0 0.5em 1.33em;
display: inline-block;
}
.subtask-details-menu {
float: right;
padding: 6px 10px 6px 10px;
}
}

View file

@ -4,12 +4,20 @@ template(name="subtasks")
| {{_ 'subtasks'}}
if currentUser.isBoardAdmin
if toggleDeleteDialog.get
.board-overlay#card-details-overlay
+subtaskDeleteDialog(subtask = subtaskToDelete)
.card-subtasks-items
each subtask in currentCard.subtasks
+subtaskDetail(subtask = subtask)
if currentCard.subtasks
.card-subtasks-items
each subtask in currentCard.subtasks
.subtask-container
+subtaskDetail(subtask = subtask)
if canModifyCard
a.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
| ☰
if currentUser.isBoardAdmin
a.js-delete-subtask-item
| ❌
if canModifyCard
+inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
@ -24,9 +32,6 @@ template(name="subtaskDetail")
+editSubtaskItemForm(subtask = subtask)
else
.subtask-title
span
if canModifyCard
a.subtask-details-menu.js-open-subtask-details-menu(title="{{_ 'subtaskActionsPopup-title'}}")
if canModifyCard
h2.title.js-open-inlined-form.is-editable
+viewer
@ -37,13 +42,13 @@ template(name="subtaskDetail")
= subtask.title
template(name="addSubtaskItemForm")
textarea.js-add-subtask-item(rows='1' autofocus dir="auto")
textarea.js-add-subtask-item(rows='1' dir="auto")
.edit-controls.clearfix
button.primary.confirm.js-submit-add-subtask-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form
template(name="editSubtaskItemForm")
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
textarea.js-edit-subtask-item(rows='1' dir="auto")
if $eq type 'item'
= item.title
else
@ -52,9 +57,6 @@ template(name="editSubtaskItemForm")
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form
span(title=createdAt) {{ moment createdAt }}
if canModifyCard
if currentUser.isBoardAdmin
a.js-delete-subtask-item {{_ "delete"}}...
template(name="subtasksItems")
.subtasks-items.js-subtasks-items
@ -100,4 +102,3 @@ template(name="subtaskActionsPopup")
a.js-delete-subtask.delete-subtask
i.fa.fa-trash
| {{_ "delete"}} ...

View file

@ -2,8 +2,8 @@
.original-position-info {
margin: 5px 0;
padding: 8px;
border-radius: 4px;
font-size: 12px;
border-radius: 0.4ch;
line-height: 1.4;
}
@ -57,7 +57,7 @@
.original-title {
color: #6c757d;
font-size: 11px;
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid #e9ecef;
@ -78,14 +78,14 @@
/* Responsive adjustments */
@media (max-width: 768px) {
.original-position-info {
font-size: 11px;
padding: 6px;
}
.original-position-details {
padding: 4px 6px;
}
.original-position-moved,
.original-position-unchanged {
padding: 3px 5px;
@ -99,24 +99,24 @@
border-color: #4a5568;
color: #e2e8f0;
}
.original-position-moved {
background-color: #744210;
border-color: #b7791f;
color: #fbd38d;
}
.original-position-unchanged {
background-color: #22543d;
border-color: #38a169;
color: #9ae6b4;
}
.original-title {
color: #a0aec0;
border-color: #4a5568;
}
.original-title strong {
color: #e2e8f0;
}

View file

@ -1,22 +1,18 @@
.datepicker-container .fields .left {
width: 56%;
}
.datepicker-container .fields .right {
width: 38%;
}
.datepicker-container .datepicker {
width: 100%;
}
.datepicker-container .datepicker table {
width: 100%;
border: none;
border-spacing: 0;
border-collapse: collapse;
}
.datepicker-container .datepicker table thead {
background: none;
}
.datepicker-container .datepicker table td,
.datepicker-container .datepicker table th {
box-sizing: border-box;
}
.datepicker-container {
form {
display: flex;
gap: 0.3lh;
padding: 0.3lh 1ch;
}
.fields {
display: flex;
justify-content: stretch;
gap: 1ch;
.left, .right {
display: flex;
flex: 1;
flex-direction: column;
align-items: stretch;
}
}
}

View file

@ -1,3 +1,16 @@
select, button, input {
font-size: 1rem !important;
}
form {
/* 🛑 remove me if it causes a significant issue.
this can be overidden and otherwise allow forms to
scale with their parent. */
display: flex;
flex-direction: column;
flex: 1;
}
select,
textarea,
input:not([type=file]),
@ -7,9 +20,8 @@ button {
border: 1px solid #ccc;
border-radius: 0.4vw;
display: block;
margin-bottom: 1.5vh;
min-height: 4.5vh;
padding: 1vh 1vw;
padding: 0.3lh 1ch;
max-width: clamp(30vw, 100%, 800px);
}
select.full,
textarea.full,
@ -42,18 +54,6 @@ input[type="text"],
input[type="password"],
input[type="email"] {
transition: background 85ms ease-in, border-color 85ms ease-in;
width: min(250px, 30vw);
}
input[type="text"].inline-input,
input[type="password"].inline-input,
input[type="email"].inline-input {
background: none;
border: 0;
margin: 0;
padding: 0.3vh;
min-height: 0;
height: 2.5vh;
width: min(200px, 25vw);
}
input[type="text"].full-line,
input[type="password"].full-line,
@ -102,11 +102,6 @@ textarea:disabled {
-webkit-user-select: none;
user-select: none;
}
select {
max-height: 40vh;
width: min(256px, 32vw);
margin-bottom: 1vh;
}
select.inline {
width: 100%;
}
@ -114,14 +109,11 @@ option[disabled] {
color: #222;
}
textarea {
height: 20vh;
transition: background 85ms ease-in, border-color 85ms ease-in;
resize: vertical;
width: 100%;
}
textarea.editor {
resize: none;
padding-bottom: 3vh;
width: auto;
font-size: 0.9em;
min-height: 3lh;
}
.button {
border-radius: 3px;
@ -137,9 +129,16 @@ button {
display: inline-block;
font-weight: 700;
line-height: 1.3;
padding: 1vh 2.5vw;
/* in flex layouts, padding often disturbs computations. rather rarely have
two lines, so setting relative-unit min-height works better */
min-height: 1.8lh;
padding: 0 2ch;
text-align: center;
color: #fff;
z-index: 1;
:not(.password-toggle-btn) {
margin-top: 0.1lh;
}
}
input[type="submit"] .wide,
button .wide {
@ -241,9 +240,9 @@ input[type="hidden"] {
}
.radio-div,
.check-div {
display: block;
margin: 0 0 0.5vh 2.5vw;
min-height: 2.5vh;
display: flex;
align-items: center;
gap: 0.2lh;
position: relative;
}
.radio-div input,
@ -260,9 +259,10 @@ input[type="hidden"] {
font-weight: 400;
}
label {
display: block;
display: flex;
flex-direction: column;
flex: 1;
font-weight: 700;
margin-bottom: 0.5vh;
}
label.form-error {
color: #d32f2f;
@ -274,11 +274,32 @@ textarea::-moz-placeholder {
color: #333 !important;
}
.edit-controls,
.add-controls {
.add-controls,
.links-controls {
display: flex;
align-items: center;
margin-top: 0px;
margin-bottom: 1.5vh;
gap: 1ch;
button {
display: flex;
justify-content: center;
align-items: center;
}
}
.edit-controls {
grid-area: main-controls;
}
.add-controls {
grid-area: main-controls;
}
.links-controls {
grid-area: links-controls
}
.links-controls span.quiet {
margin: auto;
}
@media print {
.add-controls {
@ -289,14 +310,7 @@ textarea::-moz-placeholder {
.add-controls button[type=submit],
.edit-controls input[type=button],
.add-controls input[type=button] {
float: left;
height: 4.5vh;
margin-bottom: 0px;
}
.edit-controls .fa-times-thin,
.add-controls .fa-times-thin {
font-size: clamp(20px, 4vw, 26px);
margin: 0.5vh 1.5vw;
margin: 0;
}
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
@ -306,6 +320,18 @@ textarea::-moz-placeholder {
display: none;
}
.materialCheckBox {
position: relative;
width: 0.5lh;
height: 0.5lh;
z-index: 0;
border: 0.2ch solid #5a5a5a;
border-radius: 1px;
transition: 0.2s;
margin: 0;
margin-left: 0px;
cursor: pointer;
}
.materialCheckBox:is(.active) {
position: relative;
width: 13px;
height: 13px;
@ -317,19 +343,33 @@ textarea::-moz-placeholder {
cursor: pointer;
}
.materialCheckBox.is-checked {
top: -4px;
left: -3px;
width: 7px;
height: 15px;
margin-right: 6px;
border-top: 2px solid transparent;
border-left: 2px solid transparent;
border-bottom: 2px solid #3cb500;
border-right: 2px solid #3cb500;
transform: rotate(40deg);
top: 0.3lh;
left: 0.25lh;
width: 0.25lh;
height: 0.5lh;
margin-right: 0.6lh;
border-top: 0 solid transparent;
border-left: 0 solid transparent;
border-bottom: 0.3ch solid #3cb500;
border-right: 0.3ch solid #3cb500;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform-origin: 100% 100%;
transform: rotate(50deg);
backface-visibility: hidden;
transform-origin: 0.5lh 0;
}
form .form-buttons {
display: flex;
flex: 1;
align-self: stretch;
justify-content: stretch;
gap: 0.5ch;
&>button {
display: flex;
flex: 1;
justify-content: center;
}
}
/* Grey checkmarks when grey icons setting is enabled */
body.grey-icons-enabled .materialCheckBox.is-checked {
@ -362,7 +402,7 @@ body.grey-icons-enabled .materialCheckBox.is-checked {
border-radius: 3px;
color: #fff;
display: none;
font-size: 12px;
font-weight: 700;
height: 17px;
line-height: 17px;
@ -419,7 +459,7 @@ body.grey-icons-enabled .materialCheckBox.is-checked {
.button-link.setting .label {
color: #222;
display: block;
font-size: 12px;
line-height: 14px;
margin-bottom: 0;
}
@ -428,7 +468,7 @@ body.grey-icons-enabled .materialCheckBox.is-checked {
}
.button-link.setting .value {
display: block;
font-size: 18px;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
@ -572,7 +612,7 @@ button.loud-text-button:hover {
padding: 11px;
position: relative;
text-decoration: none;
font-size: 16px;
line-height: 20px;
}
.big-link .text {
@ -615,7 +655,7 @@ button.loud-text-button:hover {
width: 40px;
}
.big-link.avatar-changer .member .member-initials {
font-size: 16px;
height: 40px;
line-height: 40px;
max-height: 40px;
@ -655,7 +695,7 @@ button.loud-text-button:hover {
left: 0;
width: 100%;
z-index: 2;
font-size: 23px;
}
.uploader .realfile input[type="file"] {
cursor: pointer;
@ -666,7 +706,7 @@ button.loud-text-button:hover {
padding: 0;
width: 100%;
z-index: 2;
font-size: 23px;
}
.uploader:hover .fakefile {
background: #318ec4;
@ -705,13 +745,13 @@ button.loud-text-button:hover {
color: #fff;
}
.material-toggle-switch {
display: flex;
padding: 0.2rlh 1ch;
align-self: center;
}
.toggle-label {
height: 0.6rlh;
width: 1.3rlh;
position: relative;
display: block;
height: 20px;
width: 44px;
background-color: #a6a6a6;
border-radius: 100px;
cursor: pointer;
@ -719,11 +759,13 @@ button.loud-text-button:hover {
}
.toggle-label:after {
position: absolute;
left: -2px;
top: -3px;
display: block;
width: 26px;
height: 26px;
/* ensure vertical centering */
margin: auto;
top: 0;
bottom: 0;
left: -0.2rlh;
width: .8rlh;
height: .8rlh;
border-radius: 100px;
background-color: #fff;
box-shadow: 0px 3px 3px rgba(0,0,0,0.05);
@ -737,7 +779,7 @@ button.loud-text-button:hover {
background-color: #6fbeb5;
}
.toggle-switch:checked ~ .toggle-label:after {
left: 20px;
left: 1.5ch;
background-color: #179588;
}
.toggle-switch:checked:disabled ~ .toggle-label {

View file

@ -52,7 +52,7 @@
min-width: 800px;
border: 2px solid #666;
font-family: sans-serif;
font-size: 13px;
background-color: #fff;
}
@ -81,7 +81,7 @@
padding: 2px 1px; /* half */
text-align: center;
background-color: #f5f5f5;
font-size: 11px;
min-width: 15px; /* half of 30px */
font-weight: bold;
height: auto;
@ -112,7 +112,7 @@
vertical-align: middle;
line-height: 28px;
background-color: #ffffff;
font-size: 18px;
font-weight: bold;
}
@ -162,7 +162,7 @@
.gantt-container tbody td.ganttview-block {
background-color: #4CAF50 !important;
color: #fff !important;
font-size: 18px !important;
font-weight: bold !important;
padding: 2px !important;
border-radius: 2px;
@ -171,7 +171,7 @@
/* Responsive adjustments */
@media (max-width: 768px) {
.gantt-container table {
font-size: 11px;
}
.gantt-container thead td {
@ -187,7 +187,7 @@
.gantt-container tbody td:first-child {
width: 100px;
font-size: 12px;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
template(name='list')
.list.js-list(id="js-list-{{_id}}"
style="{{#unless collapsed}}min-width:{{listWidth}}px;max-width:{{listConstraint}}px;{{/unless}}"
class="{{#if collapsed}}list-collapsed{{/if}} {{#if autoWidth}}list-auto-width{{/if}} {{#if isMiniScreen}}mobile-view{{/if}}")
+listHeader
unless collapsed
+listBody
.list-resize-handle.js-list-resize-handle.nodragscroll
.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}}")
+listHeader
if isCurrentList
+listBody

View file

@ -4,6 +4,8 @@ require('/client/lib/jquery-ui.js')
const { calculateIndex } = Utils;
export const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
BlazeComponent.extendComponent({
// Proxy
openForm(options) {
@ -12,6 +14,7 @@ BlazeComponent.extendComponent({
onCreated() {
this.newCardFormIsVisible = new ReactiveVar(true);
this.collapse = new ReactiveVar(Utils.getListCollapseState(this.data()));
},
// The jquery UI sortable library is the best solution I've found so far. I
@ -22,183 +25,37 @@ BlazeComponent.extendComponent({
// callback, we basically solve all issues related to reactive updates. A
// comment below provides further details.
onRendered() {
const boardComponent = this.parentComponent().parentComponent();
// Initialize list resize functionality immediately
this.list = this.firstNode();
this.resizeHandle = this.find('.js-list-resize-handle');
this.initializeListResize();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
$cards.sortable({
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
if (MultiSelection.isActive()) {
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) {
helper.append(
$(
Blaze.toHTML(
HTML.DIV(
{ class: 'and-n-other' },
TAPi18n.__('and-n-other-card', { count: andNOthers }),
),
),
),
);
}
}
return helper;
},
distance: 7,
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
scrollSpeed: 10,
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevCardDom = ui.item.prev('.js-minicard').get(0);
const nextCardDom = ui.item.next('.js-minicard').get(0);
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
const currentBoard = Utils.getCurrentBoard();
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
let targetSwimlaneId = null;
// only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
)
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
._id;
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$cards.sortable('cancel');
if (MultiSelection.isActive()) {
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(
currentBoard._id,
newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
sort(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
if (event.pageX < 10) { // scroll to the left
boardCanvas.scrollLeft -= 15;
ui.helper[0].offsetLeft -= 15;
}
if (
event.pageX > boardCanvas.offsetWidth - 10 &&
boardCanvas.scrollLeft < $boardCanvas.data('scrollLeftMax') // don't scroll more than possible
) { // scroll to the right
boardCanvas.scrollLeft += 15;
}
if (
event.pageY > boardCanvas.offsetHeight - 10 &&
event.pageY + boardCanvas.scrollTop < $boardCanvas.data('scrollTopMax') // don't scroll more than possible
) { // scroll to the bottom
boardCanvas.scrollTop += 15;
}
if (event.pageY < 10) { // scroll to the top
boardCanvas.scrollTop -= 15;
}
},
activate(event, ui) {
const $boardCanvas = $('.board-canvas');
const boardCanvas = $boardCanvas[0];
// scrollTopMax and scrollLeftMax only available at Firefox (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTopMax)
// https://www.it-swarm.com.de/de/javascript/so-erhalten-sie-den-maximalen-dokument-scrolltop-wert/1069126844/
$boardCanvas.data('scrollTopMax', boardCanvas.scrollHeight - boardCanvas.clientTop);
// https://stackoverflow.com/questions/5138373/how-do-i-get-the-max-value-of-scrollleft/5704386#5704386
$boardCanvas.data('scrollLeftMax', boardCanvas.scrollWidth - boardCanvas.clientWidth);
},
});
this.autorun(() => {
if ($cards.data('uiSortable') || $cards.data('sortable')) {
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member
!Utils.canModifyBoard(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !Utils.canModifyBoard(),
);
const ensureCollapseState = (collapsed) => {
if (this.collapse.get() === collapsed) return;
if (this.autoWidth() || collapsed) {
$(this.resizeHandle).hide();
} else {
$(this.resizeHandle).show();
}
});
this.collapse.set(collapsed);
this.initializeListResize();
}
// We want to re-run this function any time a card is added.
// Reactively update collapse appearance and resize handle visibility when auto-width or collapse changes
this.autorun(() => {
const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard');
});
Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
const card = ReactiveCache.getCard(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
card.addLabel(labelId);
}
},
});
});
ensureCollapseState(Utils.getListCollapseState(this.data()));
});
},
collapsed() {
return this.collapse.get();
},
listWidth() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!list) return 270; // Return default width if list is not available
if (user) {
// For logged-in users, get from user profile
return user.getListWidthFromStorage(list.boardId, list._id);
@ -222,8 +79,8 @@ BlazeComponent.extendComponent({
listConstraint() {
const user = ReactiveCache.getCurrentUser();
const list = Template.currentData();
if (!list) return 550; // Return default constraint if list is not available
if (!list) return 0;
if (user) {
// For logged-in users, get from user profile
return user.getListConstraintFromStorage(list.boardId, list._id);
@ -240,7 +97,7 @@ BlazeComponent.extendComponent({
} catch (e) {
console.warn('Error reading list constraint from localStorage:', e);
}
return 550; // Return default constraint if not found
return 0;
}
},
@ -256,18 +113,14 @@ BlazeComponent.extendComponent({
initializeListResize() {
// Check if we're still in a valid template context
if (!Template.currentData()) {
if (!this.data()) {
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');
if (!this.list || !this.resizeHandle) {
console.info('List or resize handle not found, retrying in 100ms');
Meteor.setTimeout(() => {
if (!this.isDestroyed) {
this.initializeListResize();
@ -275,107 +128,129 @@ BlazeComponent.extendComponent({
}, 100);
return;
}
// Reactively show/hide resize handle based on collapse and auto-width state
this.autorun(() => {
const isAutoWidth = this.autoWidth();
const isCollapsed = Utils.getListCollapseState(list);
if (isCollapsed || isAutoWidth) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
}
});
let isResizing = false;
let startX = 0;
let startWidth = 0;
let minWidth = 270; // Minimum width matching system default
let listConstraint = this.listConstraint(); // Store constraint value for use in event handlers
const component = this; // Store reference to component for use in event handlers
let previousLimit = false;
// seems reasonable; better let user shrink too much that too little
const minWidth = 280;
// stored width
const width = this.listWidth();
// min-width is initially min-content; a good start
let maxWidth = this.listConstraint() || parseInt(this.list.style.getProperty('--list-min-width', `${(minWidth)}px`), 10) || width + 100;
if (!width || width > maxWidth) {
width = (maxWidth + minWidth) / 2;
}
this.list.style.setProperty('--list-min-width', `${Math.round(minWidth)}px`);
// actual size before fitting (usually max-content equivalent)
this.list.style.setProperty('--list-max-width', `${Math.round(maxWidth)}px`);
// avoid jump effect and ensure width stays consistent
this.list.style.setProperty('--list-width', `${Math.round(width)}px`);
const component = this;
// wait for click to add other events
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');
// gain access to modern attributes e.g. isPrimary
e = e.originalEvent;
if (isResizing || Utils.shouldIgnorePointer(e)) {
return;
}
e.preventDefault();
e.stopPropagation();
$(document).on('pointermove', doResize);
// e.g. debugger can cancel event without pointerup being fired
$(document).on('pointercancel', stopResize);
$(document).on('pointerup', stopResize);
// --list-width can be either a stored size or "auto"; get actual computed size
component.currentWidth = component.list.offsetWidth;
component.list.classList.add('list-resizing');
document.body.classList.add('list-resizing-active');
isResizing = true;
};
const doResize = (e) => {
if (!isResizing) {
return;
}
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const newWidth = Math.max(minWidth, startWidth + deltaX);
// Apply the new width immediately for real-time feedback
$list[0].style.setProperty('--list-width', `${newWidth}px`);
$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');
e = e.originalEvent;
e.preventDefault();
e.stopPropagation();
if (!isResizing || !e.isPrimary) {
return;
}
if (!previousLimit && component.collapsed()) {
previousLimit = true;
component.list.classList.add('cannot-resize');
return;
}
// relative to document, always >0 because pointer sticks to the right of list
const deltaX = e.clientX - component.list.getBoundingClientRect().right;
const candidateWidth = component.currentWidth + deltaX;
component.currentWidth = Math.max(minWidth, Math.min(maxWidth, candidateWidth));
const reachingMax = (maxWidth - component.currentWidth - 20) <= 0
const reachingMin = (component.currentWidth - 20 - minWidth) <= 0
// visual indicator to avoid trying too hard; try not to apply each tick
if (!previousLimit && (reachingMax && deltaX > 0 || reachingMin && deltaX < 0)) {
component.list.classList.add('cannot-resize');
previousLimit = true;
} else if (previousLimit && !reachingMax && !reachingMin) {
component.list.classList.remove('cannot-resize');
previousLimit = false;
}
// Apply the new width immediately for real-time feedback
component.list.style.setProperty('--list-width', `${component.currentWidth}px`);
};
const stopResize = (e) => {
if (!isResizing) return;
e = e.originalEvent;
e.preventDefault();
e.stopPropagation();
if (!isResizing || !e.isPrimary) {
return;
}
// hopefully be gentler on cpu
$(document).off('pointermove', doResize);
$(document).off('pointercancel', stopResize);
$(document).off('pointerup', stopResize);
isResizing = false;
// Calculate final width
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
const deltaX = currentX - startX;
const finalWidth = Math.max(minWidth, startWidth + deltaX);
// Ensure the final width is applied
$list[0].style.setProperty('--list-width', `${finalWidth}px`);
$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');
$('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
if (previousLimit) {
component.list.classList.remove('cannot-resize');
}
const finalWidth = parseInt(component.list.style.getPropertyValue('--list-width'), 10);
// Remove visual feedback but keep the height
component.list.classList.remove('list-resizing');
document.body.classList.remove('list-resizing-active');
if (component.collapse.get()) {
return;
}
// Save the new width using the existing system
const list = component.data();
const boardId = list.boardId;
const listId = list._id;
// Use the new storage method that handles both logged-in and non-logged-in users
if (process.env.DEBUG === 'true') {
}
const currentUser = ReactiveCache.getCurrentUser();
if (currentUser) {
// For logged-in users, use server method
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, maxWidth, (error, result) => {
if (error) {
console.error('Error saving list width:', error);
} else {
@ -389,61 +264,37 @@ BlazeComponent.extendComponent({
// Save list width
const storedWidths = localStorage.getItem('wekan-list-widths');
let widths = storedWidths ? JSON.parse(storedWidths) : {};
if (!widths[boardId]) {
widths[boardId] = {};
}
widths[boardId][listId] = finalWidth;
localStorage.setItem('wekan-list-widths', JSON.stringify(widths));
// Save list constraint
const storedConstraints = localStorage.getItem('wekan-list-constraints');
let constraints = storedConstraints ? JSON.parse(storedConstraints) : {};
if (!constraints[boardId]) {
constraints[boardId] = {};
}
constraints[boardId][listId] = listConstraint;
localStorage.setItem('wekan-list-constraints', JSON.stringify(constraints));
if (process.env.DEBUG === 'true') {
}
} catch (e) {
console.warn('Error saving list width/constraint to localStorage:', e);
}
}
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();
});
// Reactively update resize handle visibility when auto-width or collapse changes
component.autorun(() => {
const collapsed = Utils.getListCollapseState(list);
if (component.autoWidth() || collapsed) {
$resizeHandle.hide();
} else {
$resizeHandle.show();
}
});
// handle both pointer and touch
$(this.resizeHandle).on("pointerdown", startResize);
// Clean up on component destruction
component.onDestroyed(() => {
@ -455,12 +306,6 @@ BlazeComponent.extendComponent({
},
}).register('list');
Template.list.helpers({
collapsed() {
return Utils.getListCollapseState(this);
},
});
Template.miniList.events({
'click .js-select-list'() {
const listId = this._id;
@ -468,15 +313,10 @@ Template.miniList.events({
},
});
// Enable drag-reorder for collapsed lists from .js-collapsed-list-drag area
this.$('.js-collapsed-list-drag').draggable({
axis: 'x',
helper: 'clone',
revert: 'invalid',
start(evt, ui) {
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
}
});
Template.miniList.helpers({
isCurrentList() {
const currentList = Utils.getCurrentList();
const list = Template.currentData();
return currentList && currentList._id == list._id;
},
});

View file

@ -4,17 +4,18 @@ template(name="listBody")
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
+inlinedForm(autoclose=false position="top")
+addCardForm(listId=_id position="top")
ul.sidebar-list
each customFieldsSum
li
+viewer
= name
if $eq customFieldsSum.type "number"
if customFieldSum.lenght
ul.sidebar-list
each customFieldsSum
li
+viewer
= value
if $eq customFieldsSum.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(value)
= name
if $eq customFieldsSum.type "number"
+viewer
= value
if $eq customFieldsSum.type "currency"
+viewer
= formattedCurrencyCustomFieldValue(value)
each (cardsWithLimit (idOrNull ../../_id))
a.minicard-wrapper.js-minicard(href=originRelativeUrl
class="{{#if cardIsSelected}}is-selected{{/if}}"
@ -25,15 +26,15 @@ template(name="listBody")
+minicard(this)
if (showSpinner (idOrNull ../../_id))
+spinnerList
if canSeeAddCard
+inlinedForm(autoclose=false position="bottom")
+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
| {{_ 'add-card'}}
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
a.minicard-wrapper.minicard-add-form
+inlinedForm(autoclose=false position="bottom")
+addCardForm(listId=_id position="bottom")
else
.add-card-wrapper
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(
@ -43,33 +44,30 @@ template(name="spinnerList")
template(name="addCardForm")
.minicard.minicard-composer.js-composer
if getLabels
.minicard-labels
each getLabels
.minicard-label(class="card-label-{{color}}" title="{{name}}")
textarea.minicard-composer-textarea.js-card-title(autofocus dir="auto")
if members.get
.minicard-members.js-minicard-composer-members
each members.get
+userAvatar(userId=this)
.minicard-bottom
.minicard-composer-icons
if getLabels
each getLabels
.minicard-label(class="card-label-{{color}}" title="{{name}}")
if members.get
each members.get
+userAvatar(userId=this)
.add-controls.clearfix
a.js-close-inlined-form
i.fa.fa-times-thin
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
a.js-close-inlined-form
i.fa.fa-times-thin
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
.links-controls.clearfix
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-link {{_ 'link'}}
span.quiet
| &nbsp;
| /
a.js-search {{_ 'search'}}
span.quiet
| &nbsp;
| /
a.js-card-template {{_ 'template'}}
template(name="autocompleteLabelLine")
@ -77,70 +75,73 @@ template(name="autocompleteLabelLine")
span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
template(name="linkCardPopup")
label {{_ 'boards'}}:
.link-board-wrapper
select.js-select-boards
option(value="")
each boards
option(value="{{_id}}") {{isTitleDefault title}}
input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}")
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
option(value="") {{_ 'custom-field-dropdown-none'}}
each swimlanes
option(value="{{_id}}") {{isTitleDefault title}}
label {{_ 'lists'}}:
select.js-select-lists
option(value="") {{_ 'custom-field-dropdown-none'}}
each lists
option(value="{{_id}}") {{isTitleDefault title}}
label {{_ 'cards'}}:
select.js-select-cards
option(value="") {{_ 'custom-field-dropdown-none'}}
each cards
option(value="{{getRealId}}") {{getTitle}}
.edit-controls.clearfix
input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
template(name="searchElementPopup")
form
label
| {{_ 'title'}}
input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required dir="auto")
unless isTemplateSearch
label {{_ 'boards'}}:
.link-board-wrapper
.link-board-dropdown
label {{_ 'boards'}}:
select.js-select-boards
option(value="")
each boards
option(value="{{_id}}") {{title}}
form.js-search-term-form
label
| {{_ 'template'}}
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
.list-body.search-card-results
.minicards.clearfix.js-minicards
if isBoardTemplateSearch
each results
a.minicard-wrapper.js-minicard
+miniboard(this)
if isListTemplateSearch
each results
a.minicard-wrapper.js-minicard
+minilist(this)
if isSwimlaneTemplateSearch
each results
a.minicard-wrapper.js-minicard
+miniswimlane(this)
if isCardTemplateSearch
each results
a.minicard-wrapper.js-minicard
+minicard(this)
unless isTemplateSearch
each results
a.minicard-wrapper.js-minicard
+minicard(this)
option(value="{{_id}}") {{isTitleDefault title}}
input.primary.confirm.js-link-board(type="button" value="{{_ 'link'}}")
.link-board-dropdown
label {{_ 'swimlanes'}}:
select.js-select-swimlanes
option(value="") {{_ 'custom-field-dropdown-none'}}
each swimlanes
option(value="{{_id}}") {{isTitleDefault title}}
.link-board-dropdown
label {{_ 'lists'}}:
select.js-select-lists
option(value="") {{_ 'custom-field-dropdown-none'}}
each lists
option(value="{{_id}}") {{isTitleDefault title}}
.link-board-dropdown
label {{_ 'cards'}}:
select.js-select-cards
option(value="") {{_ 'custom-field-dropdown-none'}}
each cards
option(value="{{getRealId}}") {{getTitle}}
.edit-controls.clearfix
input.primary.confirm.js-done(type="button" value="{{_ 'link'}}")
template(name="searchElementPopup")
.link-board-wrapper
form
label
| {{_ 'title'}}
input.js-element-title(type="text" placeholder="{{_ 'title'}}" autofocus required dir="auto")
unless isTemplateSearch
label {{_ 'boards'}}:
select.js-select-boards
option(value="")
each (boards)
option(value="{{_id}}") {{title}}
form.js-search-term-form
label
| {{_ 'template'}}
input(type="text" name="searchTerm" placeholder="{{_ 'search-example'}}" autofocus dir="auto")
.list-body.search-card-results
.minicards.clearfix.js-minicards
if isBoardTemplateSearch
each (results)
a.minicard-wrapper.js-minicard
+miniboard(this)
if isListTemplateSearch
each (results)
a.minicard-wrapper.js-minicard
+minilist(this)
if isSwimlaneTemplateSearch
each (results)
a.minicard-wrapper.js-minicard
+miniswimlane(this)
if isCardTemplateSearch
each (results)
a.minicard-wrapper.js-minicard
+minicard(this)
unless isTemplateSearch
each (results)
a.minicard-wrapper.js-minicard
+minicard(this)

View file

@ -3,16 +3,168 @@ import { TAPi18n } from '/imports/i18n';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { Spinner } from '/client/lib/spinner';
import getSlug from 'limax';
import { itemsSelector } from './list';
const subManager = new SubsManager();
const InfiniteScrollIter = 10;
function sortableCards(boardComponent, $cards) {
return {
connectWith: '.js-minicards:not(.js-list-full)',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
const cardHeight = item.height();
const cardWidth = item.width();
helper[0].setAttribute('style', `height: ${cardHeight}px !important; width: ${cardWidth}px !important;`);
if (MultiSelection.isActive()) {
const andNOthers = $cards.find('.js-minicard.is-checked').length - 1;
if (andNOthers > 0) {
helper.append(
$(
Blaze.toHTML(
HTML.DIV(
{ class: 'and-n-other' },
TAPi18n.__('and-n-other-card', { count: andNOthers }),
),
),
),
);
}
}
return helper;
},
distance: 7,
items: itemsSelector,
placeholder: 'minicard-wrapper placeholder',
/* cursor must be tied to smaller objects, position approximately from the button
(can be computed if visually confusing) */
cursorAt: { right: 20, top: 30 },
start(evt, ui) {
const cardHeight = ui.helper.height();
ui.placeholder[0].setAttribute('style', `height: ${cardHeight}px !important;`);
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following card -- if any.
const prevCardDom = ui.item.prev('.js-minicard').get(0);
const nextCardDom = ui.item.next('.js-minicard').get(0);
const nCards = MultiSelection.isActive() ? MultiSelection.count() : 1;
const sortIndex = Utils.calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list-body').get(0))._id;
const currentBoard = Utils.getCurrentBoard();
const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
let targetSwimlaneId = null;
// only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
)
targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
._id;
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
// (especially when we move the last card of a list, or when multiple
// users move some cards at the same time). To prevent these UX glitches
// we ask sortable to gracefully cancel the move, and to put back the
// DOM in its initial state. The card move is then handled reactively by
// Blaze with the below query.
$cards.sortable('cancel');
if (MultiSelection.isActive()) {
ReactiveCache.getCards(MultiSelection.getMongoSelector(), { sort: ['sort'] }).forEach((card, i) => {
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(
currentBoard._id,
newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
});
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
const newSwimlaneId = targetSwimlaneId
? targetSwimlaneId
: card.swimlaneId || defaultSwimlaneId;
card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
sort(event, ui) {
Utils.scrollIfNeeded(event);
},
};
};
BlazeComponent.extendComponent({
onCreated() {
// for infinite scrolling
this.cardlimit = new ReactiveVar(InfiniteScrollIter);
},
onRendered() {
// Prefer handling drag/sort in listBody rather than list as
// it is shared between mobile and desktop view
const boardComponent = BlazeComponent.getComponentForElement(document.getElementsByClassName('board-canvas')[0]);
const $cards = this.$('.js-minicards');
$cards.sortable(sortableCards(boardComponent, $cards));
this.autorun(() => {
if ($cards.data('uiSortable') || $cards.data('sortable')) {
// Use handle button on mobile, classic move otherwise
if (Utils.isMiniScreen()) {
$cards.sortable('option', 'handle', '.handle');
} else {
$cards.sortable('option', 'handle', '.minicard');
}
$cards.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member
!Utils.canModifyBoard(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !Utils.canModifyBoard(),
);
}
});
// We want to re-run this function any time a card is added.
this.autorun(() => {
const currentBoardId = Tracker.nonreactive(() => {
return Session.get('currentBoard');
});
Tracker.afterFlush(() => {
$cards.find(itemsSelector).droppable({
hoverClass: 'draggable-hover-card',
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
const card = ReactiveCache.getCard(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
card.addLabel(labelId);
}
},
});
});
});
},
mixins() {
return [];
},
@ -82,9 +234,10 @@ BlazeComponent.extendComponent({
evt.preventDefault();
const firstCardDom = this.find('.js-minicard:first');
const lastCardDom = this.find('.js-minicard:last');
const textarea = $(evt.currentTarget).find('textarea');
// more robust to start from the form
const textarea = $(evt.currentTarget).closest('.inlined-form').find('textarea');
const position = this.currentData().position;
const title = textarea.val().trim();
const title = $(textarea).val().trim();
let sortIndex;
if (position === 'top') {
@ -168,7 +321,6 @@ BlazeComponent.extendComponent({
// We keep the form opened, empty it, and scroll to it.
textarea.val('').focus();
autosize.update(textarea);
if (position === 'bottom') {
this.scrollToBottom();
}
@ -194,21 +346,19 @@ BlazeComponent.extendComponent({
clickOnMiniCard(evt) {
if (MultiSelection.isActive() || evt.shiftKey) {
evt.stopImmediatePropagation();
evt.preventDefault();
const methodName = evt.shiftKey ? 'toggleRange' : 'toggle';
MultiSelection[methodName](this.currentData()._id);
// If the card is already selected, we want to de-select it.
// XXX We should probably modify the minicard href attribute instead of
// overwriting the event in case the card is already selected.
} else if (Utils.isMiniScreen()) {
evt.preventDefault();
Session.set('popupCardId', this.currentData()._id);
this.cardDetailsPopup(evt);
} else if (Session.equals('currentCard', this.currentData()._id)) {
evt.stopImmediatePropagation();
evt.preventDefault();
// We need to wait a little because router gets called first,
// we probably need a level of indirection
// #FIXME remove if it works with commits we rebased on,
// which change the route declaration order
Meteor.setTimeout(() => {
Session.set('currentCard', null)
}, 50);
Utils.goBoardId(Session.get('currentBoard'));
} else {
// Allow normal href navigation, but if it's the same card URL,
@ -283,12 +433,6 @@ BlazeComponent.extendComponent({
return user && user.isVerticalScrollbars();
},
cardDetailsPopup(event) {
if (!Popup.isOpen()) {
Popup.open("cardDetails")(event);
}
},
events() {
return [
{
@ -296,6 +440,8 @@ BlazeComponent.extendComponent({
'click .js-toggle-multi-selection': this.toggleMultiSelection,
'click .open-minicard-composer': this.scrollToBottom,
submit: this.addCard,
// #FIXME remove in final MR if it works
'click .confirm': this.addCard
},
];
},
@ -401,6 +547,17 @@ BlazeComponent.extendComponent({
'click .js-link': Popup.open('linkCard'),
'click .js-search': Popup.open('searchElement'),
'click .js-card-template': Popup.open('searchElement'),
submit: this.addCard,
'click .minicard-label': (event) => {
const clickedData = BlazeComponent.getComponentForElement(event.target).currentData?.()
this.labels.set(this.labels.get().filter(e => e !== clickedData?._id));
},
'click .member': (event) => {
const clickedData = BlazeComponent.getComponentForElement(event.target).currentData?.()
this.members.set(this.members.get().filter(e => e !== clickedData?.userId));
e.preventDefault();
e.stopPropagation();
},
},
];
},
@ -409,8 +566,6 @@ BlazeComponent.extendComponent({
const editor = this;
const $textarea = this.$('textarea');
autosize($textarea);
$textarea.escapeableTextComplete(
[
// User mentions
@ -421,7 +576,9 @@ BlazeComponent.extendComponent({
callback(
$.map(currentBoard.activeMembers(), member => {
const user = ReactiveCache.getUser(member.userId);
return user.username.indexOf(term) === 0 ? user : null;
return user.username.indexOf(term) === 0 &&
// don't show already selected members
!editor.members.get().find((e) => e === member.userId) ? user : null;
}),
);
},
@ -445,8 +602,12 @@ BlazeComponent.extendComponent({
const currentBoard = Utils.getCurrentBoard();
callback(
$.map(currentBoard.labels, label => {
if (label.name == undefined) {
label.name = "";
if (
label.name == undefined ||
// don't show already selected labels
editor.getLabels().find((e) => e._id === label._id)
) {
return null;
}
if (
label.name.indexOf(term) > -1 ||
@ -503,10 +664,10 @@ BlazeComponent.extendComponent({
subManager.subscribe('board', this.boardId, false);
this.board = ReactiveCache.getBoard(this.boardId);
// List where to insert card
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
this.list = $(PopupComponent.stack[0].openerElement).closest('.js-list');
this.listId = Blaze.getData(this.list[0])._id;
// Swimlane where to insert card
const swimlane = $(Popup._getTopStack().openerElement).closest(
const swimlane = $(PopupComponent.stack[0].openerElement).closest(
'.js-swimlane',
);
this.swimlaneId = '';
@ -539,10 +700,10 @@ BlazeComponent.extendComponent({
if (!board) {
return [];
}
// Ensure default swimlane exists
board.getDefaultSwimline();
const swimlanes = ReactiveCache.getSwimlanes(
{
boardId: this.selectedBoardId.get()
@ -559,7 +720,8 @@ BlazeComponent.extendComponent({
}
const lists = ReactiveCache.getLists(
{
boardId: this.selectedBoardId.get()
boardId: this.selectedBoardId.get(),
swimlaneId: this.selectedSwimlaneId?.get?.()
},
{
sort: { sort: 1 },
@ -703,16 +865,16 @@ BlazeComponent.extendComponent({
},
onCreated() {
this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
this.isCardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
'js-card-template',
);
this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
this.isListTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
'js-list-template',
);
this.isSwimlaneTemplateSearch = $(
Popup._getTopStack().openerElement,
PopupComponent.stack[0].openerElement,
).hasClass('js-open-add-swimlane-menu');
this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
this.isBoardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
'js-add-board',
);
this.isTemplateSearch =
@ -731,20 +893,16 @@ BlazeComponent.extendComponent({
} else {
this.board = Utils.getCurrentBoard();
}
if (!this.board) {
Popup.back();
return;
}
this.boardId = this.board._id;
this.boardId = this.board?._id;
// Subscribe to this board
subManager.subscribe('board', this.boardId, false);
this.selectedBoardId = new ReactiveVar(this.boardId);
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
if (!this.isBoardTemplateSearch) {
this.list = $(PopupComponent.stack[0].openerElement).closest('.js-list');
this.swimlaneId = '';
// Swimlane where to insert card
const swimlane = $(Popup._getTopStack().openerElement).parents(
const swimlane = $(PopupComponent.stack[0].openerElement).parents(
'.js-swimlane',
);
if (Utils.boardView() === 'board-view-swimlanes')
@ -783,11 +941,7 @@ BlazeComponent.extendComponent({
} else if (this.isSwimlaneTemplateSearch) {
return board.searchSwimlanes(this.term.get());
} else if (this.isBoardTemplateSearch) {
const boards = board.searchBoards(this.term.get());
boards.forEach(board => {
subManager.subscribe('board', board.linkedId, false);
});
return boards;
return board.searchBoards(this.term.get());
} else {
return [];
}

View file

@ -9,66 +9,68 @@ template(name="listHeader")
if currentList
a.list-header-left-icon.js-unselect-list
i.fa.fa-caret-left
else
if collapsed
if showCardsCountForList cards.length
br
span.cardCount {{cardsCount}}
if isMiniScreen
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
if wipLimit.enabled
|&nbsp;(
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|/#{wipLimit.value})
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
else
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
if collapsed
else
//- start by this on mobile to have cohesion with other views
a.list-header-menu-icon.js-select-list
i.fa.fa-caret-right
else
i.fa.fa-caret-down
div(class="{{#if collapsed}}list-rotated{{/if}}")
.list-header-name-container
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
if wipLimit.enabled
|&nbsp;(
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|/#{wipLimit.value})
|&nbsp;(
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|/#{wipLimit.value})
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
else
div.list-header-name-container
unless isMiniScreen
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
if collapsed
i.fa.fa-caret-right
else
i.fa.fa-caret-down
div(class="{{#if collapsed}}list-rotated{{/if}}").list-header-wrap
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
if wipLimit.enabled
|&nbsp;(
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|/#{wipLimit.value})
unless collapsed
if showCardsCountForList cards.length
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
if hasNumberFieldsSum
| &nbsp;
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
div.list-header-menu
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
if isMiniScreen
if currentList
if isWatching
i.list-header-watch-icon i.fa.fa-eye
i.list-header-watch-icon.i.fa.fa-eye
div.list-header-menu
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
i.fa.fa-plus
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
else
a.list-header-menu-icon.js-select-list
i.fa.fa-caret-right
unless currentUser.isWorker
if isTouchScreenOrShowDesktopDragHandles
if isMiniScreen
a.list-header-handle.handle.js-list-handle
i.fa.fa-arrows
else if currentUser.isBoardMember
@ -77,24 +79,13 @@ template(name="listHeader")
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
if isTouchScreenOrShowDesktopDragHandles
if isMiniScreen
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
i.fa.fa-arrows
unless collapsed
div.list-header-menu
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
//if isBoardAdmin
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
if isTouchScreenOrShowDesktopDragHandles
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
i.fa.fa-arrows
if canSeeAddCard
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}")
i.fa.fa-plus
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}")
i.fa.fa-bars
unless isMiniScreen
if collapsed
if showCardsCountForList cards.length
span.cardCount {{cardsCount}}
template(name="editListTitleForm")
.list-composer
@ -224,14 +215,14 @@ template(name="wipLimitErrorPopup")
.wip-limit-invalid
p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
button.full.js-back-view(type="submit") {{_ 'cancel'}}
button.negate.js-back-view(type="submit") {{_ 'cancel'}}
template(name="setListWidthPopup")
#js-list-width-edit
label {{_ 'set-list-width-value'}}
p
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270")
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="270")
input.list-width-value(type="number" value="{{ listWidthValue }}" min="100")
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="100")
input.list-width-apply(type="submit" value="{{_ 'apply'}}")
input.list-width-error
br
@ -242,8 +233,8 @@ template(name="setListWidthPopup")
template(name="listWidthErrorPopup")
.list-width-invalid
p {{_ 'list-width-error-message'}} '&gt;=270'
button.full.js-back-view(type="submit") {{_ 'cancel'}}
p {{_ 'list-width-error-message'}} '&gt;=100'
button.negate.js-back-view(type="submit") {{_ 'cancel'}}
template(name="setListColorPopup")
form.edit-label

View file

@ -9,6 +9,15 @@ Meteor.startup(() => {
});
BlazeComponent.extendComponent({
onRendered() {
/* #FIXME I have no idea why this exact same
event won't fire when in event maps */
$(this.find('.js-collapse')).on('click', (e) => {
e.preventDefault();
this.collapsed(!this.collapsed());
});
},
canSeeAddCard() {
const list = Template.currentData();
return (
@ -34,7 +43,7 @@ BlazeComponent.extendComponent({
}
},
collapsed(check = undefined) {
const list = Template.currentData();
const list = this.data();
const status = Utils.getListCollapseState(list);
if (check === undefined) {
// just check
@ -110,7 +119,11 @@ BlazeComponent.extendComponent({
return TAPi18n.__('cards-count');
}
},
currentList() {
const currentList = Utils.getCurrentList();
const list = Template.currentData();
return currentList && currentList._id == list._id;
},
events() {
return [
{
@ -118,10 +131,6 @@ BlazeComponent.extendComponent({
event.preventDefault();
this.starred(!this.starred());
},
'click .js-collapse'(event) {
event.preventDefault();
this.collapsed(!this.collapsed());
},
'click .js-open-list-menu': Popup.open('listAction'),
'click .js-add-card.list-header-plus-top'(event) {
const listDom = $(event.target).parents(
@ -459,10 +468,10 @@ BlazeComponent.extendComponent({
this.currentBoard = Utils.getCurrentBoard();
this.currentSwimlaneId = new ReactiveVar(null);
this.currentListId = new ReactiveVar(null);
// Get the swimlane context from opener
const openerData = Popup.getOpenerComponent()?.data();
// If opened from swimlane menu, openerData is the swimlane
if (openerData?.type === 'swimlane' || openerData?.type === 'template-swimlane') {
this.currentSwimlane = openerData;
@ -554,4 +563,3 @@ BlazeComponent.extendComponent({
];
},
}).register('addListPopup');

View file

@ -1,6 +1,6 @@
.my-cards-board-wrapper {
border-radius: 0 0 0.5vw 0.5vw;
min-width: min(400px, 52vw);
min-width: min(100%, 400px, 52vw);
margin-bottom: 2.5vh;
margin-right: auto;
margin-left: auto;
@ -33,13 +33,6 @@
text-align: center;
margin-bottom: 0.9vh;
}
.my-cards-list-wrapper {
margin: 1.3vh 1.3vw;
border-radius: 0.7vw;
display: inline-grid;
min-width: min(250px, 32vw);
max-width: min(350px, 45vw);
}
.my-cards-card-wrapper {
margin-top: 0;
margin-bottom: 1.3vh;
@ -81,7 +74,7 @@
}
.accessibility-page h2 {
font-size: 24px;
margin-bottom: 20px;
color: #4d4d4d;
}

View file

@ -1,19 +1,18 @@
.new-comment a.fa.fa-brands.fa-markdown,
.inlined-form a.fa.fa-brands.fa-markdown {
float: right;
position: absolute;
top: -10px;
right: 60px;
.new-comment, .inlined-form {
a.fa.fa-brands.fa-markdown, a.fa.fa-copy {
display: flex;
justify-content: end;
}
}
.new-comment a.fa.fa-copy,
.inlined-form a.fa.fa-copy {
float: right;
position: relative;
top: -10px;
right: 5px;
}
.js-inlined-form.viewer.btn-sm {
position: absolute;
top: 20px;
right: 6px;
.editor-controls {
display: flex;
justify-content: end;
grid-area: editor-controls;
align-items: center;
align-self: start;
gap: 1ch;
}
.editor {
grid-area: editor;
}

View file

@ -1,12 +1,12 @@
template(name="editor")
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip {{_ 'copied'}}
.editor-controls
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
span.copied-tooltip.copied-tooltip-hidden {{_ 'copied'}}
textarea.editor(
dir="auto"
class="{{class}}"
id=id
autofocus=autofocus
placeholder="{{_ 'comment-placeholder'}}")
+Template.contentBlock

View file

@ -90,7 +90,6 @@ BlazeComponent.extendComponent({
const enableTextarea = function() {
const $textarea = this.$(textareaSelector);
autosize($textarea);
$textarea.escapeableTextComplete(mentions);
};
if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === true || Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === 'true') {

View file

@ -1,6 +1,6 @@
.global-search-board-wrapper {
border-radius: 8px;
min-width: 400px;
border-radius: 0.8ch;
min-width: min(100%, 400px);
border-width: 8px;
border-color: #808080;
border-style: solid;
@ -67,8 +67,6 @@
color: #8b0000;
}
.global-search-page {
width: 40%;
min-width: 400px;
margin-right: auto;
margin-left: auto;
line-height: 150%;
@ -91,6 +89,13 @@
font-family: Courier;
font-style: italic;
}
.lists-wrapper {
display: flex;
flex-wrap: wrap;
gap: 1ch 0.3lh;
}
code {
color: #000;
background-color: #d3d3d3;

File diff suppressed because it is too large Load diff

View file

@ -5,100 +5,81 @@ template(name="header")
Reddit "subreddit" bar.
The first link goes to the boards page.
if currentUser
#header-quick-access(class=currentBoard.colorClass)
#header-quick-access(class="currentBoard.colorClass {{#if isMiniScreen}}mobile-view{{/if}}")
// Home icon - always at left side of logo
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
i.fa.fa-home
| {{_ 'all-boards'}}
#header-quick-access-left
span.home-icon.allBoards
a(href="{{pathFor 'home'}}")
span.emoji-icon
i.fa.fa-home
span
| {{_ 'all-boards'}}
// Logo - visible; on mobile constrained by CSS
unless currentSetting.hideLogo
if currentSetting.customTopLeftCornerLogoImageUrl
if currentSetting.customTopLeftCornerLogoLinkUrl
a(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0")
unless currentSetting.customTopLeftCornerLogoLinkUrl
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" height="{{#if currentSetting.customTopLeftCornerLogoHeight}}#{currentSetting.customTopLeftCornerLogoHeight}{{else}}27{{/if}}" width="auto" margin="0" padding="0" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
unless currentSetting.customTopLeftCornerLogoImageUrl
div#headerIsSettingDatabaseCallDone
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
// Zoom controls - always visible
.zoom-controls
span.zoom-level.js-zoom-level-click(title="{{_ 'click-to-change-zoom'}}")
span.zoom-display {{zoomLevel}}%
input.zoom-input.js-zoom-input(type="number" value=zoomLevel min="50" max="300" step="10" style="display: none;")
// Drag handles toggle - between zoom and mobile mode toggle
a.board-header-btn.js-toggle-desktop-drag-handles(title="{{_ 'show-desktop-drag-handles'}}")
i.fa.fa-arrows
if isShowDesktopDragHandles
i.fa.fa-check
unless isShowDesktopDragHandles
i.fa.fa-ban
if isMiniScreen
ul.header-quick-access-list
if currentList
each currentBoard.lists
li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
a.js-select-list
+viewer
= title
else
if isMiniScreen
ul.header-quick-access-list
if currentList
each currentBoard.lists
li(class="{{#if $.Session.equals 'currentList' _id}}current{{/if}}")
a.js-select-list.
+viewer
= title
else
each currentUser.starredBoards
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
a(href="{{pathFor 'board' id=_id slug=slug}}")
+viewer
= title
else
ul.header-quick-access-list
//li
// a(href="{{pathFor 'public'}}")
// span.fa.fa-globe
// | {{_ 'public'}}
each currentUser.starredBoards
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
a(href="{{pathFor 'board' id=_id slug=slug}}")
+viewer
= title
else
li.current.empty(title="{{_ 'quick-access-description'}}")
| {{_ 'quick-access-description'}}
#header-new-board-icon
// Next line is used only for spacing at header,
// there is no visible clickable icon.
#header-new-board-icon
else
ul.header-quick-access-list
//li
// a(href="{{pathFor 'public'}}")
// span.fa.fa-globe
// | {{_ 'public'}}
each currentUser.starredBoards
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
a(href="{{pathFor 'board' id=_id slug=slug}}")
+viewer
= title
// Hide duplicate create board button,
// because it did not show board templates correctly.
//a#header-new-board-icon.js-create-board
// i.fa.fa-plus(title="Create a new board")
// Logo - visible; on mobile constrained by CSS
unless currentSetting.hideLogo
.logo-container
if currentSetting.customTopLeftCornerLogoImageUrl
if currentSetting.customTopLeftCornerLogoLinkUrl
a.logo(href="{{currentSetting.customTopLeftCornerLogoLinkUrl}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
+logo
else
+logo
else
li.current.empty(title="{{_ 'quick-access-description'}}")
| {{_ 'quick-access-description'}}
#header-new-board-icon
// Next line is used only for spacing at header,
// there is no visible clickable icon.
#header-new-board-icon
// Hide duplicate create board button,
// because it did not show board templates correctly.
//a#header-new-board-icon.js-create-board
// i.fa.fa-plus(title="Create a new board")
div#headerIsSettingDatabaseCallDone.logo
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
.mobile-mode-toggle
a.board-header-btn.js-mobile-mode-toggle(title="{{_ 'mobile-desktop-toggle'}}" class="{{#if mobileMode}}mobile-active{{else}}desktop-active{{/if}}")
i.mobile-icon(class="{{#if mobileMode}}active{{/if}}")
i.fa.fa-mobile
i.desktop-icon(class="{{#unless mobileMode}}active{{/unless}}")
i.fa.fa-desktop
// Notifications
+notifications
if currentSetting.customHelpLinkUrl
#header-help
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
i.fa.fa-question-circle
+headerUserBar
#header-quick-access-right
if currentSetting.customHelpLinkUrl
#header-help
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
i.fa.fa-question-circle
#header-quick-access-icons
+headerUserBar
// Notifications
+notifications
#header(class=currentBoard.colorClass)
//-
The main bar is a colorful bar that provide all the meta-data for the
current page. This bar is contextual based.
If the user is not connected we display "sign in" and "log in" buttons.
#header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
#header-main-bar(class="{{#if isMiniScreen}}mobile-view{{/if}} {{#if wrappedHeader}}wrapper{{/if}}")
+Template.dynamic(template=headerBar)
if appIsOffline
@ -122,3 +103,7 @@ template(name="offlineWarning")
| {{_ 'app-is-offline'}}
a.app-try-reconnect {{_ 'app-try-reconnect'}}
//- a little helper to avoid duplication
template(name="logo")
img(src="{{currentSetting.customTopLeftCornerLogoImageUrl}}" style="{{#if currentSetting.customTopLeftCornerLogoHeight}}min-height: #{currentSetting.customTopLeftCornerLogoHeight};{{/if}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")

View file

@ -22,13 +22,13 @@ Template.header.onCreated(function () {
)
document.getElementById(
'headerIsSettingDatabaseCallDone',
).style.display = 'none';
).style.visibility = 'hidden';
else if (
document.getElementById('headerIsSettingDatabaseCallDone') != null
)
document.getElementById(
'headerIsSettingDatabaseCallDone',
).style.display = 'block';
).style.visibility = 'visible';
return this.stop();
},
});
@ -57,14 +57,6 @@ Template.header.helpers({
return announcements && announcements.body;
},
zoomLevel() {
const sessionZoom = Session.get('wekan-zoom-level');
if (sessionZoom !== undefined) {
return Math.round(sessionZoom * 100);
}
return Math.round(Utils.getZoomLevel() * 100);
},
mobileMode() {
const sessionMode = Session.get('wekan-mobile-mode');
if (sessionMode !== undefined) {
@ -76,51 +68,6 @@ Template.header.helpers({
Template.header.events({
'click .js-create-board': Popup.open('headerBarCreateBoard'),
'click .js-zoom-level-click'(evt) {
const $zoomDisplay = $(evt.currentTarget).find('.zoom-display');
const $zoomInput = $(evt.currentTarget).find('.zoom-input');
// Hide display, show input
$zoomDisplay.hide();
$zoomInput.show().focus().select();
},
'keypress .js-zoom-input'(evt) {
if (evt.which === 13) {
// Enter key
const newZoomPercent = parseInt(evt.target.value);
if (
!isNaN(newZoomPercent) &&
newZoomPercent >= 50 &&
newZoomPercent <= 300
) {
const newZoom = newZoomPercent / 100;
Utils.setZoomLevel(newZoom);
// Hide input, show display
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
const $zoomInput = $(evt.target);
$zoomInput.hide();
$zoomDisplay.show();
} else {
alert('Please enter a zoom level between 50% and 300%');
evt.target.focus().select();
}
}
},
'blur .js-zoom-input'(evt) {
// When input loses focus, hide it and show display
const $zoomDisplay = $(evt.target).siblings('.zoom-display');
const $zoomInput = $(evt.target);
$zoomInput.hide();
$zoomDisplay.show();
},
'click .js-mobile-mode-toggle'() {
const currentMode = Utils.getMobileMode();
Utils.setMobileMode(!currentMode);
},
'click .js-open-bookmarks'(evt) {
// Already added but ensure single definition -- safe guard
},

View file

@ -12,7 +12,7 @@
.shortcuts-list .shortcuts-list-item .shortcuts-list-item-keys kbd {
padding: 5px 8px;
margin: 5px;
font-size: 18px;
}
.shortcuts-list .shortcuts-list-item .shortcuts-list-item-action {
font-size: 1.4em;

View file

@ -1,7 +1,33 @@
* {
-webkit-box-sizing: unset;
box-sizing: unset;
/* Global variables that we can use to easily test and change layout
Later it could be useful to use a CSS superset */
/* this makes the property computable */
@property --popup-margin {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
:root {
scroll-behavior: smooth;
--label-height: 1.7lh;
--header-scale: clamp(1rem, 1.333rem + -0.333vw, 1.3rem)
--popup-margin: 2vmax;
/* regarding fonts, this is one of the clearest I found: https://modern-fluid-typography.vercel.app/ */
&:has(body.desktop-mode) {
font-size: clamp(1rem, 1.68rem + -0.57vw, 1.4rem);
--quick-header-scale: clamp(0.8rem, 0.6rem + 0.4vw, 1.2rem);
--list-item-size: 1.2em;
}
&:has(body.mobile-mode) {
font-size: clamp(2.5rem, 3vw + 1.7rem, 3.5rem);
--quick-header-scale: 1.3em;
--header-scale: clamp(1rem, -0.5vw + 1.25rem, 1.125rem);
--list-item-size: 1.6em;
}
}
/* Fixed missing 'import nib' stylesheet reset and extra li bullet points
* https://github.com/wekan/wekan/issues/4512#issuecomment-1129347536
*/
@ -32,29 +58,26 @@ a:focus {
color: unset;
text-decoration: unset;
}
.badge {
display: unset;
min-width: unset;
padding: unset;
font-size: unset;
font-weight: unset;
line-height: unset;
color: unset;
text-align: unset;
white-space: unset;
vertical-align: unset;
background-color: unset;
border-radius: unset;
display: flex;
gap: 0 0.3ch;
align-items: center;
}
body {
/* changed programmatically on swimlane resizes, or e.g. when un-collapsed */
transition: height 0.2s ease-out, width 0.2s ease-out;
}
html,
body,
input,
select,
textarea,
button {
font: clamp(14px, 2.5vw, 18px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 1.4;
color: #4d4d4d;
font-family: Roboto, Poppins, "Helvetica Neue", "Liberation Sans", Arial, Helvetica, sans-serif;
color: hsl(0, 0%, 30%);
/* Improve text rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@ -63,58 +86,74 @@ button {
user-select: text;
}
html {
font-size: 100%;
max-height: 100%;
-webkit-user-select: text;
user-select: text;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
text-size-adjust: 100%;
overscroll-behavior: none;
}
body {
background: #dedede;
margin: 0;
position: relative;
z-index: 0;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
height: 100vh;
/* iOS Safari fixes */
-webkit-overflow-scrolling: touch;
align-items: stretch;
justify-content: start;
/* height is auto; if set to 100vh, it prevents navbar to disappear on scroll... */
width: 100%;
/* Needs to be set on body and html. Feels ok to disable entirely as Wekan is really drag/scroll-heavy */
overscroll-behavior: none;
min-height: 100vh;
line-height: 1.4;
}
/* Mobile mode specific fixes for iOS Safari */
body.mobile-mode {
overflow-x: hidden;
position: fixed;
width: 100%;
height: 100vh;
/* Prevent iOS Safari bounce scroll */
overscroll-behavior: none;
-webkit-overflow-scrolling: touch;
}
/* Ensure content area is scrollable in mobile mode */
body.mobile-mode #content {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
height: calc(100vh - 48px);
}
/* Prevent scroll through popups */
body:has(.pop-over:hover) {
overflow: hidden;
}
/* Some forms will need extra adjustement (removing margins, etc)
but it worth it to let browsers take care of exact placement/sizing */
.inlined-form {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: center;
gap: 0.3lh;
width: 100%;
}
#content {
display: flex;
position: relative;
flex: 1;
overflow-x: hidden;
margin-bottom: 1vh;
min-height: 100vh;
max-width: min(100%, 100vw);
}
#content .sk-spinner {
margin-top: 30vh;
}
#content > .wrapper {
margin-top: 1vh;
padding: 2vh 2vw;
}
#modal {
position: absolute;
top: 0;
@ -157,25 +196,6 @@ body.mobile-mode #content {
#modal .modal-content-wide .modal-close-btn {
display: block;
float: right;
font-size: clamp(18px, 4vw, 24px);
}
h1 {
font-size: clamp(18px, 4vw, 24px);
line-height: 1.2em;
margin: 0 0 1vh;
}
h2 {
font-size: clamp(16px, 3.5vw, 20px);
line-height: 1.2em;
margin: 0 0 0.8vh;
}
h3,
h4,
h5,
h6 {
font-size: clamp(14px, 3vw, 18px);
line-height: 1.25em;
margin: 0 0 0.6vh;
}
.quiet,
.quiet a {
@ -226,7 +246,7 @@ p {
}
p a {
text-decoration: underline;
word-wrap: break-word;
overflow-wrap: break-word;
}
table,
p {
@ -250,13 +270,13 @@ blockquote {
padding: 0 0 0 1vw;
}
hr {
height: 1px;
height: 0.2ch;
border: 0;
border: none;
width: 100%;
background: #dbdbdb;
color: #dbdbdb;
margin: 2vh 0;
margin: 0.2lh 0;
padding: 0;
}
table,
@ -303,7 +323,7 @@ kbd {
clear: both;
}
.hide {
display: none;
display: none !important;
}
.show {
display: block;
@ -337,8 +357,11 @@ kbd {
padding-bottom: 0;
}
.wrapper {
width: calc(100% - 2vw);
margin: 0 auto;
margin: 0;
flex: 1;
width: auto;
height: fit-content;
display: grid;
}
.relative {
position: relative;
@ -369,8 +392,12 @@ kbd {
.invisible {
visibility: hidden;
}
.invisible-line {
height: 1.3lh;
visibility: hidden;
}
.wrapword {
word-wrap: break-word;
overflow-wrap: break-word;
}
.grab {
cursor: grab;
@ -445,8 +472,39 @@ a:not(.disabled).is-active i.fa {
}
.viewer {
min-height: 2.5vh;
display: block;
word-wrap: break-word;
display: flex;
flex-direction: column;
align-items: start;
justify-content: center;
/* a tentative to get layout less dependant of content,
especially for small elements e.g. labels: the goal is that
content will be cut with `...` if too large (but will be fully
rendered in dedicated interfaces)
the classic technique is to use flex-basis, but it depends
on the parent not overflowing to get the right size; also,
specifying in terms of lines makes the browser act clever, by
fitting the available space and cutting after N lines, whatever
is the text's length */
min-width: 0;
p, ul {
margin: 0;
padding: 0;
text-overflow: ellipsis;
overflow: hidden;
/* See https: //css-tricks.com/line-clampin/,
it is widely supported and waiting standardization https: //caniuse.com/?search=-webkit-line-clamp */
display: -webkit-box !important;
/* 0 has no effect; ensures will not interfere unless asked */
-webkit-line-clamp: var(--overflow-lines, 0);
-webkit-box-orient: vertical;
-webkit-align-items: center;
/* grid properties apply */
align-content: center;
word-break: break-word;
white-space: normal;
}
}
.viewer table {
word-wrap: normal;
@ -481,6 +539,12 @@ a:not(.disabled).is-active i.fa {
padding: 0;
padding-top: 15px;
}
.basicTabs-container .tabs-list .tab-item {
/* where does templates_tabs.css come from? visible in
devtools but not in sources */
font-size: unset !important;
}
.no-scrollbars {
scrollbar-width: none;
}
@ -495,133 +559,30 @@ a:not(.disabled).is-active i.fa {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: landscape),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) and (orientation: portrait) {
#content {
margin: 1px 0px 0px 0px;
height: calc(100% - 0px);
/* Improve touch scrolling */
-webkit-overflow-scrolling: touch;
}
#content > .wrapper {
margin-top: 0px;
padding: 8px;
}
.wrapper {
height: calc(100% - 31px);
margin: 0px;
padding: 8px;
}
.panel-default {
width: 95vw;
max-width: 95vw;
margin: 0 auto;
}
/* Improve touch targets */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 44px;
min-width: 44px;
padding: 12px 16px;
font-size: 16px; /* Prevent zoom on iOS */
/* Prevent zoom on iOS */
touch-action: manipulation;
}
/* Form elements */
input, select, textarea {
font-size: 16px; /* Prevent zoom on iOS */
/* Prevent zoom on iOS */
padding: 12px;
min-height: 44px;
touch-action: manipulation;
}
/* Cards and lists */
.minicard {
min-height: 48px;
padding: 12px;
margin-bottom: 8px;
touch-action: manipulation;
}
.list {
margin: 0 8px;
min-width: 280px;
}
/* Board canvas */
.board-canvas {
padding: 0 8px 8px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* Header mobile layout */
#header {
padding: 8px;
/* Keep top bar on a single row on small screens */
flex-wrap: nowrap;
align-items: center;
gap: 8px;
}
#header-quick-access {
/* Keep quick-access items in one row */
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
width: 100%;
}
/* Hide elements that should move to the hamburger menu on mobile */
#header-quick-access .header-quick-access-list,
#header-quick-access #header-help {
display: none !important;
}
/* Show only the home icon (hide the trailing text) on mobile */
#header-quick-access .home-icon a {
display: inline-flex;
align-items: center;
max-width: 28px; /* enough to display the icon */
overflow: hidden;
white-space: nowrap;
}
/* Hide text in home icon on mobile, show only icon */
#header-quick-access .home-icon a span:not(.fa) {
display: none !important;
}
/* Ensure proper spacing for mobile header elements */
#header-quick-access .zoom-controls {
margin-left: auto;
margin-right: 8px;
}
.mobile-mode-toggle {
margin-right: 8px;
}
#header-user-bar {
margin-left: auto;
}
/* Ensure header elements don't wrap on very small screens */
#header-quick-access {
min-width: 0; /* Allow flexbox to shrink */
}
/* Make sure logo doesn't take too much space on mobile */
#header-quick-access img {
max-height: 24px;
max-width: 120px;
}
/* Ensure zoom controls are compact on mobile */
.zoom-controls .zoom-level {
padding: 4px 8px;
font-size: 12px;
}
/* Modal mobile optimization */
#modal .modal-content,
#modal .modal-content-wide {
@ -632,29 +593,28 @@ a:not(.disabled).is-active i.fa {
max-height: 90vh;
overflow-y: auto;
}
/* Table mobile optimization */
table {
font-size: 14px;
width: 100%;
display: block;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
/* Admin panel mobile optimization */
.setting-content .content-body {
flex-direction: column;
gap: 16px;
padding: 8px;
}
.setting-content .content-body .side-menu {
width: 100%;
order: 2;
}
.setting-content .content-body .main-body {
order: 1;
min-height: 60vh;
@ -663,139 +623,175 @@ a:not(.disabled).is-active i.fa {
}
}
<<<<<<< HEAD
/* Tablet devices (768px - 1024px) */
@media screen and (min-width: 768px) and (max-width: 1024px) {
#content > .wrapper {
padding: 12px;
}
.wrapper {
padding: 12px;
}
.panel-default {
width: 90vw;
max-width: 90vw;
}
/* Touch-friendly but more compact */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 48px;
min-width: 48px;
padding: 10px 14px;
}
.minicard {
min-height: 40px;
padding: 10px;
}
.list {
margin: 0 12px;
min-width: 300px;
}
.board-canvas {
padding: 0 12px 12px 0;
}
#header {
padding: 12px 16px;
}
#modal .modal-content {
width: 80vw;
max-width: 600px;
}
#modal .modal-content-wide {
width: 90vw;
max-width: 800px;
}
.setting-content .content-body {
gap: 20px;
}
.setting-content .content-body .side-menu {
width: 250px;
}
/* Responsive handling for quick-access description on tablets */
#header-quick-access ul.header-quick-access-list li.current.empty {
max-width: 300px;
}
}
||||||| parent of 2e0149f79 (🚧 Remove zoom/mobile option, rework header/misc layout to be more responsive)
/* Tablet devices (768px - 1024px) */
@media screen and (min-width: 768px) and (max-width: 1024px) {
#content > .wrapper {
padding: 12px;
}
.wrapper {
padding: 12px;
}
.panel-default {
width: 90vw;
max-width: 90vw;
}
/* Touch-friendly but more compact */
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 48px;
min-width: 48px;
padding: 10px 14px;
}
.minicard {
min-height: 40px;
padding: 10px;
}
.list {
margin: 0 12px;
min-width: 300px;
}
.board-canvas {
padding: 0 12px 12px 0;
}
#header {
padding: 12px 16px;
}
#modal .modal-content {
width: 80vw;
max-width: 600px;
}
#modal .modal-content-wide {
width: 90vw;
max-width: 800px;
}
.setting-content .content-body {
gap: 20px;
}
.setting-content .content-body .side-menu {
width: 250px;
}
}
=======
>>>>>>> 2e0149f79 (🚧 Remove zoom/mobile option, rework header/misc layout to be more responsive)
/* Large displays and digital signage (1920px+) */
@media screen and (min-width: 1920px) {
body {
font-size: 18px;
}
button, .btn, .js-toggle, .js-color-choice, .js-reaction, .close {
min-height: 56px;
min-width: 56px;
padding: 16px 20px;
font-size: 18px;
}
.minicard {
min-height: 56px;
padding: 16px;
font-size: 18px;
}
.list {
margin: 0 8px;
min-width: 360px;
}
.board-canvas {
padding: 0;
}
#header {
padding: 0 8px;
}
#content > .wrapper {
padding: 0;
}
#modal .modal-content {
width: 600px;
}
#modal .modal-content-wide {
width: 1000px;
}
.setting-content .content-body {
gap: 32px;
}
.setting-content .content-body .side-menu {
width: 320px;
}
}
.inline-input {
height: 37px;
margin: 8px 10px 0 0;
width: 100px;
.ui-sortable-handle {
cursor: grab !important;
}
.select-authentication {
width: 100%;
}
.textBelowCustomLoginLogo,
.auth-layout {
#rescue-card-description {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.auth-layout .auth-dialog {
margin: 0 !important;
flex: 1 0 auto;
align-self: center;
margin: 0 0.2lh;
}
.loadingText {
text-align: center;
@ -882,8 +878,18 @@ a:not(.disabled).is-active i.fa {
text-decoration: underline;
text-decoration-color: #17683a;
}
/*
Prevents popups to compute real size, trying to comment
.at-pwd-form, .at-sep, .at-oauth {
display: none;
}*/
#at-pwd-form {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: stretch;
gap: 0.3lh;
}
@-moz-keyframes fadeIn {
from {
@ -928,31 +934,19 @@ a:not(.disabled).is-active i.fa {
/* iOS Safari Mobile Mode Fixes */
@media screen and (max-width: 800px) {
/* Prevent scrolling issues on iOS Safari when card popup is open */
body.mobile-mode {
overflow: hidden;
position: fixed;
width: 100%;
height: 100vh;
}
/* Fix z-index stacking for mobile Safari */
body.mobile-mode .board-wrapper {
z-index: 1;
}
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
z-index: 17 !important;
}
body.mobile-mode .card-details {
z-index: 100 !important;
}
body.mobile-mode .pop-over {
z-index: 999;
}
/* Ensure smooth scrolling on iOS */
body.mobile-mode .card-details,
body.mobile-mode .pop-over .content-wrapper {

View file

@ -23,61 +23,56 @@ template(name="main")
//link(rel="stylesheet" type="text/css" class="__meteor-css__" href="css/html5-default-theme.css")
template(name="userFormsLayout")
section.auth-layout
if currentSetting.hideLogo
h1.at-form-landing-logo
br
br
unless currentSetting.hideLogo
h1.at-form-landing-logo
if currentSetting.customLoginLogoImageUrl
if currentSetting.customLoginLogoLinkUrl
a(href="{{currentSetting.customLoginLogoLinkUrl}}")
img(src="{{currentSetting.customLoginLogoImageUrl}}" width="300" height="auto")
.auth-container
section.auth-layout.auth-logo
if currentSetting.hideLogo
h1.at-form-landing-logo
unless currentSetting.hideLogo
if currentSetting.customLoginLogoImageUrl
if currentSetting.customLoginLogoLinkUrl
a(href="{{currentSetting.customLoginLogoLinkUrl}}")
img(src="{{currentSetting.customLoginLogoImageUrl}}")
unless currentSetting.customLoginLogoLinkUrl
a
img(src="{{currentSetting.customLoginLogoImageUrl}}")
else
a
img(src="{{pathFor '/wekan-logo.svg'}}" alt="")
br
unless currentSetting.customLoginLogoLinkUrl
img(src="{{currentSetting.customLoginLogoImageUrl}}" width="300" height="auto")
br
else
img(src="{{pathFor '/wekan-logo.svg'}}" alt="" width="300" height="auto")
br
if currentSetting.textBelowCustomLoginLogo
hr
section.textBelowCustomLoginLogo
+viewer
| {{currentSetting.textBelowCustomLoginLogo}}
hr
section.auth-layout
section.auth-dialog
if isLoading
+loader
else
// ARIA live region for error messages
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f; margin-bottom: 1em;")
+Template.dynamic(template=content)
if currentSetting.displayAuthenticationMethod
+connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
if isLegalNoticeLinkExist
div#legalNoticeDiv
span#legalNoticeSpan {{_ 'acceptance_of_our_legalNotice'}}
a#legalNoticeAtLink.at-link(href="{{currentSetting.legalNotice}}", target="_blank", rel="noopener noreferrer")
| {{_ 'legalNotice'}}
if getLegalNoticeWithWritTraduction
div
div.at-form-lang
label(for="userform-set-language-select") {{_ 'changeLanguagePopup-title'}}
select.select-lang.js-userform-set-language#userform-set-language-select(aria-label="{{_ 'changeLanguagePopup-title'}}")
each languages
if isCurrentLanguage
if rtl
option(value="{{tag}}" selected="selected") {{name}} (RTL)
section.auth-custom-text
if currentSetting.textBelowCustomLoginLogo
section.textBelowCustomLoginLogo
+viewer
| {{currentSetting.textBelowCustomLoginLogo}}
section.auth-layout.auth-form
section.auth-dialog
if isLoading
+loader
else
// ARIA live region for error messages
div#login-error-message(role="alert" aria-live="assertive" style="color: #d32f2f;")
+Template.dynamic(template=content)
if currentSetting.displayAuthenticationMethod
+connectionMethod(authenticationMethod=currentSetting.defaultAuthenticationMethod)
if isLegalNoticeLinkExist
div#legalNoticeDiv
span#legalNoticeSpan {{_ 'acceptance_of_our_legalNotice'}}
a#legalNoticeAtLink.at-link(href="{{currentSetting.legalNotice}}", target="_blank", rel="noopener noreferrer")
| {{_ 'legalNotice'}}
div.at-form-lang
label(for="userform-set-language-select") {{_ 'changeLanguagePopup-title'}}
select.select-lang.js-userform-set-language#userform-set-language-select(aria-label="{{_ 'changeLanguagePopup-title'}}")
each languages
if isCurrentLanguage
if rtl
option(value="{{tag}}" selected="selected") {{name}} (RTL)
else
option(value="{{tag}}" selected="selected") {{name}}
else
option(value="{{tag}}" selected="selected") {{name}}
else
if rtl
option(value="{{tag}}") {{name}} (RTL)
else
option(value="{{tag}}") {{name}}
if rtl
option(value="{{tag}}") {{name}} (RTL)
else
option(value="{{tag}}") {{name}}
template(name="defaultLayout")
+header

View file

@ -1,22 +1,18 @@
.my-cards-board-wrapper {
border-radius: 0 0 0.5vw 0.5vw;
min-width: min(400px, 52vw);
margin-bottom: 2.5vh;
margin-right: auto;
margin-left: auto;
border-width: 0.3vw;
border-style: solid;
border-color: #a2a2a2;
body.mobile-mode {
.my-cards-board-wrapper {
width: 100vw;
}
.my-cards-swimlane-body {
grid-auto-flow: row;
}
}
.my-cards-board-title {
font-size: clamp(1.2rem, 3vw, 1.6rem);
font-weight: bold;
padding: 0.7vh 0.7vw;
background-color: #808080;
color: #fff;
.my-cards-swimlane-body {
display: grid;
grid-auto-flow: column;
gap: 1ch;
}
.my-cards-swimlane-title {
font-size: clamp(1rem, 2.5vw, 1.3rem);
font-size: clamp(1em, 2.5vw, 1.3rem);
font-weight: bold;
padding: 0.7vh 0.7vw;
padding-bottom: 0.5vh;
@ -27,48 +23,12 @@
.swimlane-default-color {
background-color: #d3d3d3;
}
.my-cards-list-title {
font-weight: bold;
font-size: clamp(1rem, 2.5vw, 1.3rem);
text-align: center;
margin-bottom: 0.9vh;
}
.my-cards-list-wrapper {
margin: 1.3vh 1.3vw;
border-radius: 0.7vw;
display: inline-grid;
min-width: min(250px, 32vw);
max-width: min(350px, 45vw);
display: flex;
flex-direction: column;
max-width: clamp(300px, 20vw, 30vw);
}
.my-cards-card-wrapper {
margin-top: 0;
margin-bottom: 1.3vh;
}
.my-cards-dueat-list-wrapper {
max-width: min(500px, 65vw);
margin-right: auto;
margin-left: auto;
}
.my-cards-board-table thead {
border-bottom: 3px solid #4d4d4d;
background-color: transparent;
}
.my-cards-board-table th,
.my-cards-board-table td {
border: 0;
}
.my-cards-board-table tr {
border-bottom: 2px solid #a2a2a2;
}
.my-cards-card-title-table {
font-weight: bold;
padding-left: 2px;
max-width: 243px;
}
.my-cards-board-badge {
width: 36px;
height: 24px;
float: left;
border-radius: 5px;
margin-right: 5px;
body.mobile-mode .my-cards-list-wrapper {
max-width: unset;
}

View file

@ -39,15 +39,16 @@ template(name="myCards")
.my-cards-swimlane-title(class="{{#if swimlane.colorClass}}{{ swimlane.colorClass }}{{else}}swimlane-default-color{{/if}}")
+viewer
= swimlane.title
each list in swimlane.myLists
.my-cards-list-wrapper
.my-cards-list-title(class=list.colorClass)
+viewer
= list.title
each card in list.myCards
.my-cards-card-wrapper
a.minicard-wrapper(href=card.originRelativeUrl)
+minicard(card)
.my-cards-swimlane-body
each list in swimlane.myLists
.my-cards-list-wrapper
.my-cards-list-title(class=list.colorClass)
+viewer
= list.title
each card in list.myCards
.my-cards-card-wrapper
a.minicard-wrapper(href=card.originRelativeUrl)
+minicard(card)
if $eq myCardsView 'table'
.wrapper
table.my-cards-board-table

View file

@ -1,91 +1,121 @@
.pop-over {
background: #fff;
border-radius: 0.4vw;
border: 1px solid #dbdbdb;
background: #ededed;
border-bottom-color: #c2c2c2;
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
position: absolute;
/* Wider default to fit full color palette */
width: min(380px, 55vw);
z-index: 99999;
margin-top: 0.7vh;
box-shadow: 0 0.2vh 0.8vh rgba(0, 0, 0, 0.3);
/* so they can easily travel with mouse */
position: fixed;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
align-items: stretch;
resize: both;
pointer-events: all;
max-height: 100vh;
.content-wrapper {
width: auto;
height: auto;
position: relative;
overflow-y: auto;
}
.content-wrapper >* {
/* low specificity so that it can be transparently overriden,
but could have side effects if no display is explicitely specific in inner content */
display: flex;
flex: 1;
flex-direction: column;
width: auto;
height: auto;
}
}
.pop-over a:has(.fa-plus)+ :not(*) {
min-height: 1.5lh;
aspect-ratio: 1/1;
display: flex;
justify-content: center;
margin-top: 0.2lh;
}
.pop-over hr {
margin: 0.5vh 0px;
margin: 0.3lh 0;
/* below everything in the same stacking context when
after, child or explicit z-index */
z-index: 0;
}
.pop-over p,
.pop-over textarea,
.pop-over input[type="text"],
.pop-over input[type="email"],
.pop-over input[type="password"],
.pop-over input[type="file"] {
width: 100%;
.pop-over {
/* feels like it's too ad-hod */
input, a:not(.js-board-template, .member, .edit-avatar) {
display: inline-flex;
align-items: center;
gap: 1ch;
min-height: 1.5lh;
}
}
.pop-over select {
width: 100%;
margin-bottom: 1.8vh;
}
.pop-over textarea {
height: 9vh;
}
.pop-over form a span {
padding: 0 0.7vw;
.pop-over .sub-name {
max-width: clamp(30vw, 500px, 80%);
}
.pop-over .header {
height: 4.5vh;
position: relative;
margin-bottom: 1vh;
display: flex;
justify-content: space-between;
gap: 1ch;
align-items: center;
padding: 0 1ch;
background: #f7f7f7;
border-bottom: 1px solid #dcdcdc;
color: #666;
min-height: 2lh;
}
.pop-over .header .header-title {
display: block;
line-height: 4vh;
padding-top: 0.5vh;
margin: 0 1.3vw;
display: flex;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.2em;
flex: 1;
cursor: grab !important;
}
.pop-over .header .back-btn {
.pop-over .back-btn {
float: left;
overflow: hidden;
width: 4vw;
transition: width 0.2s;
}
.pop-over .header .back-btn i.fa {
margin: 1.3vw;
margin-top: 1.5vh;
}
.pop-over .header .back-btn.is-hidden {
.pop-over .back-btn.is-hidden {
width: 0;
}
.pop-over .header .close-btn {
padding: 1.3vh 1.3vw 1.3vh 0.5vw;
position: absolute;
top: 0;
right: 0;
}
.pop-over.no-title .header {
background: none;
}
.pop-over .content-wrapper {
width: 100%;
max-height: calc(70vh + 20px);
overflow-y: auto;
overflow-x: hidden;
.pop-over {
.content-wrapper, .header {
display: flex;
align-items: center;
}
}
/* Allow dynamic max-height to override default constraint */
.pop-over[style*="max-height"] .content-wrapper {
max-height: inherit;
.pop-over:has(.header) .content {
/* inner content has full width available,
so it is also responsive for margins, sizes, etc */
overflow-y: auto;
}
.popup-placeholder {
/* This gives relative coordinates but height/width cannot fit the parent's
without it having position: relative; we need to get them programmatically */
position: absolute;
/* Take all size of parent so it can be useful in computations */
visibility: hidden;
display: none;
}
.pop-over .content-container {
width: 100%;
max-height: calc(70vh + 20px);
transition: transform 0.2s;
display: flex;
align-items: stretch;
flex: 1;
}
/* Allow dynamic max-height to override default constraint for content-container */
@ -93,270 +123,42 @@
max-height: inherit;
}
/* Fix overflow in the Member Settings (member menu) popup:
the popup itself gets a max-height inline style, but the header consumes space.
Make the header overlay the scrollable area so the list can't spill out. */
.pop-over[data-popup="memberMenuPopup"] {
overflow: hidden;
}
.pop-over[data-popup="memberMenuPopup"] > .header {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-bottom: 0;
z-index: 1;
}
.pop-over[data-popup="memberMenuPopup"] > .content-wrapper {
padding-top: calc(4.5vh + 1vh);
box-sizing: border-box;
.pop-over .popup-drag-handle {
cursor: move;
}
/* Admin edit popups: use full height */
.pop-over[data-popup="editUserPopup"],
.pop-over[data-popup="editOrgPopup"],
.pop-over[data-popup="editTeamPopup"] {
height: calc(100vh - 20px) !important;
max-height: calc(100vh - 20px) !important;
body.mobile-mode {
.popup-drag-handle, .close-btn {
font-size: 1.4em;
align-self: center;
}
.pop-over:has(.pop-over-list) {
min-width: 70vw;
}
}
.pop-over[data-popup="editUserPopup"] .content-wrapper,
.pop-over[data-popup="editOrgPopup"] .content-wrapper,
.pop-over[data-popup="editTeamPopup"] .content-wrapper {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
overflow-y: auto !important;
.pop-over .header-controls {
display: flex;
gap: 1ch;
}
.pop-over[data-popup="editUserPopup"] .content-container,
.pop-over[data-popup="editOrgPopup"] .content-container,
.pop-over[data-popup="editTeamPopup"] .content-container {
max-height: calc(100vh - 80px) !important; /* Subtract header height */
height: calc(100vh - 80px) !important;
}
/* Ensure language popup list can scroll properly */
.pop-over .pop-over-list {
max-height: none;
overflow: visible;
}
/* Specific styling for language popup list */
.pop-over[data-popup="changeLanguagePopup"] .pop-over-list {
max-height: none;
overflow: visible;
height: auto;
flex: 1;
}
/* Ensure content div in language popup contains all items */
.pop-over[data-popup="changeLanguagePopup"] .content {
height: auto;
/* Remove forced min-height to avoid top gap */
display: flex;
flex-direction: column;
flex: 1;
font-size: 1.1rem;
padding: 0 1ch;
>li>a {
display: grid;
grid-auto-flow: column;
grid-auto-columns: fit-content;
justify-content: start;
padding: 0 0.5ch;
column-gap: 1ch;
.sub-name {
text-align: end;
}
}
}
/* Ensure hidden stack pages truly take no space */
.pop-over[data-popup="changeLanguagePopup"] .content.no-height {
min-height: 0 !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
visibility: hidden !important;
}
/* Make language popup extend to bottom of browser window */
.pop-over[data-popup="changeLanguagePopup"] {
position: fixed !important;
bottom: 0 !important;
top: auto !important;
left: auto !important;
right: 20px !important;
width: auto !important;
max-width: 450px !important;
height: 100vh !important;
max-height: 100vh !important;
min-height: 300px !important;
display: flex !important;
flex-direction: column !important;
margin: 0 !important;
}
/* Allow dynamic height for Change Language popup */
.pop-over[data-popup="changeLanguagePopup"] .header {
flex-shrink: 0 !important;
height: auto !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content-wrapper {
flex: 1 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
min-height: 0 !important;
max-height: none !important;
height: auto !important;
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content-container {
height: auto !important;
max-height: none !important;
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
width: 100% !important;
}
.pop-over[data-popup="changeLanguagePopup"] .content {
height: auto !important;
max-height: none !important;
padding-bottom: 50px !important;
width: 100% !important;
}
/* Date popup sizing for native HTML inputs */
.pop-over[data-popup="editCardReceivedDatePopup"],
.pop-over[data-popup="editCardStartDatePopup"],
.pop-over[data-popup="editCardDueDatePopup"],
.pop-over[data-popup="editCardEndDatePopup"],
.pop-over[data-popup*="Date"] {
width: min(400px, 90vw) !important; /* Smaller width for native inputs */
min-width: 350px !important;
max-height: 80vh !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardStartDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardDueDatePopup"] .content-wrapper,
.pop-over[data-popup="editCardEndDatePopup"] .content-wrapper,
.pop-over[data-popup*="Date"] .content-wrapper {
max-height: 60vh !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .content-container,
.pop-over[data-popup="editCardStartDatePopup"] .content-container,
.pop-over[data-popup="editCardDueDatePopup"] .content-container,
.pop-over[data-popup="editCardEndDatePopup"] .content-container,
.pop-over[data-popup*="Date"] .content-container {
max-height: 60vh !important;
}
/* Native HTML input styling */
.pop-over[data-popup*="Date"] .datepicker-container {
width: 100% !important;
padding: 15px !important;
}
.pop-over[data-popup*="Date"] .datepicker-container .fields {
display: flex !important;
gap: 15px !important;
margin-bottom: 15px !important;
}
.pop-over[data-popup*="Date"] .datepicker-container .fields .left,
.pop-over[data-popup*="Date"] .datepicker-container .fields .right {
flex: 1 !important;
width: auto !important;
}
.pop-over[data-popup*="Date"] .datepicker-container label {
display: block !important;
margin-bottom: 5px !important;
font-weight: bold !important;
}
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"],
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"] {
width: 100% !important;
padding: 8px !important;
border: 1px solid #ccc !important;
border-radius: 4px !important;
font-size: 14px !important;
box-sizing: border-box !important;
}
.pop-over[data-popup*="Date"] .datepicker-container input[type="date"]:focus,
.pop-over[data-popup*="Date"] .datepicker-container input[type="time"]:focus {
outline: none !important;
border-color: #007cba !important;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2) !important;
}
/* Ensure date popup buttons stay within popup boundaries */
.pop-over[data-popup="editCardReceivedDatePopup"] .content,
.pop-over[data-popup="editCardStartDatePopup"] .content,
.pop-over[data-popup="editCardDueDatePopup"] .content,
.pop-over[data-popup="editCardEndDatePopup"] .content,
.pop-over[data-popup*="Date"] .content {
max-height: 60vh !important; /* Leave space for buttons */
overflow-y: auto !important;
padding-bottom: 100px !important; /* More space for buttons */
margin-bottom: 0 !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardStartDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardDueDatePopup"] .datepicker-container,
.pop-over[data-popup="editCardEndDatePopup"] .datepicker-container,
.pop-over[data-popup*="Date"] .datepicker-container {
max-height: 50vh !important; /* Limit calendar height */
overflow-y: auto !important;
margin-bottom: 20px !important; /* Space before buttons */
}
/* Ensure buttons are properly positioned */
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date,
.pop-over[data-popup*="Date"] .edit-date {
display: flex !important;
flex-direction: column !important;
height: 100% !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .fields,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .fields,
.pop-over[data-popup*="Date"] .edit-date .fields {
flex-shrink: 0 !important;
margin-bottom: 15px !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date .js-datepicker,
.pop-over[data-popup*="Date"] .edit-date .js-datepicker {
flex: 1 !important;
overflow-y: auto !important;
}
.pop-over[data-popup="editCardReceivedDatePopup"] .edit-date button,
.pop-over[data-popup="editCardStartDatePopup"] .edit-date button,
.pop-over[data-popup="editCardDueDatePopup"] .edit-date button,
.pop-over[data-popup="editCardEndDatePopup"] .edit-date button,
.pop-over[data-popup*="Date"] .edit-date button {
flex-shrink: 0 !important;
margin-top: 15px !important;
position: relative !important;
z-index: 10 !important;
}
.pop-over .content-container .content {
/* Match wider popover, leave padding */
width: 100%;
padding: 0 1.3vw 1.3vh;
box-sizing: border-box;
/* Ensure content is not shifted left */
margin-left: 0 !important;
transform: none !important;
}
/* Utility: remove left gutter inside specific popups */
.pop-over .content .flush-left {
margin-left: 0;
@ -378,58 +180,15 @@
.pop-over .content form.create-label .palette-colors {
margin-left: 0;
padding-left: 0;
width: 100%;
display: grid;
grid-template-columns: repeat(5, 1fr);
}
/* Color palette items: ensure proper positioning */
.pop-over .content .palette-colors .palette-color {
margin-left: 0;
margin-right: 2px;
margin-bottom: 2px;
}
/* Global fix for all popup content to prevent left shifting */
.pop-over .content * {
margin-left: 0 !important;
transform: none !important;
}
/* Override any potential left shifting for specific elements */
.pop-over .content form,
.pop-over .content .palette-colors,
.pop-over .content .pop-over-list,
.pop-over .content .flush-left {
margin-left: 0 !important;
padding-left: 0 !important;
transform: none !important;
}
/* Fix popup depth containers that cause left shifting */
.pop-over .popup-container-depth-1,
.pop-over .popup-container-depth-2,
.pop-over .popup-container-depth-3,
.pop-over .popup-container-depth-4,
.pop-over .popup-container-depth-5,
.pop-over .popup-container-depth-6 {
transform: none !important;
margin-left: 0 !important;
padding-left: 0 !important;
}
/* Ensure buttons dont reserve left space; align to flow */
.pop-over .content form.swimlane-color-popup .primary.confirm,
.pop-over .content form.swimlane-color-popup .negate.wide.right,
.pop-over .content .swimlane-height-popup .primary.confirm,
.pop-over .content .swimlane-height-popup .negate.wide.right {
float: none;
margin-left: 0;
}
.pop-over .content-container .content.no-height {
height: 0;
overflow: hidden;
padding: 0;
margin: 0;
visibility: hidden;
border-radius: 0;
outline: 0.1ch solid black;
}
.pop-over.search-over {
background: #f0f0f0;
@ -456,24 +215,6 @@
.pop-over .sk-spinner {
margin: 40px auto;
}
.pop-over .popup-container-depth-1 {
transform: translateX(-300px);
}
.pop-over .popup-container-depth-2 {
transform: translateX(-600px);
}
.pop-over .popup-container-depth-3 {
transform: translateX(-900px);
}
.pop-over .popup-container-depth-4 {
transform: translateX(-1200px);
}
.pop-over .popup-container-depth-5 {
transform: translateX(-1500px);
}
.pop-over .popup-container-depth-6 {
transform: translateX(-1800px);
}
.select-members-list,
.select-avatars-list {
margin-bottom: 8px;
@ -487,15 +228,12 @@
cursor: pointer;
display: block;
font-weight: 700;
padding: 1.5px 10px;
padding-inline: 2vmin 10vmin;
position: relative;
margin: 0;
text-decoration: none;
overflow: hidden;
line-height: 33px;
display:flex;
/* flex-wrap:wrap;*/
gap:5px;
align-items: center;
color: #000 !important;
}
@ -506,7 +244,6 @@
.pop-over-list li > a .item-name {
display: block;
width: auto;
padding-right: 22px;
}
.pop-over-list li > a:not(.disabled):hover {
background-color: #005377;
@ -522,9 +259,9 @@
.pop-over-list li > a .sub-name {
color: #8c8c8c;
display: block;
font-size: 12px;
font-size: 0.8em;
font-weight: 400;
line-height: 15px;
line-height: 1.2em;
}
.pop-over-list li > a.current {
background-color: #e2e6e9;
@ -570,156 +307,21 @@
body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
color: #7a7a7a;
}
.pop-over.miniprofile .header {
border-bottom-color: transparent;
height: 30px;
position: absolute;
right: 0;
top: 0;
width: 60px;
z-index: 1;
}
.pop-over.miniprofile .header-title {
display: none;
}
.pop-over.miniprofile .pop-over-list {
padding-top: 8px;
}
.pop-over.miniprofile .miniprofile-header {
margin-top: 8px;
min-height: 56px;
position: relative;
}
.pop-over.miniprofile .miniprofile-header .member,
.pop-over.miniprofile .miniprofile-header .avatar {
position: absolute;
top: 2px;
left: 2px;
height: 50px;
width: 50px;
}
.pop-over.miniprofile .miniprofile-header .info {
margin: 0 0 0 64px;
word-wrap: break-word;
}
.pop-over.miniprofile .miniprofile-header .info h3 a {
text-decoration: none;
}
.pop-over.miniprofile .miniprofile-header .info h3 a:hover {
text-decoration: underline;
}
@media screen and (max-width: 800px) {
.pop-over {
width: 100%;
height: 100%;
overflow: hidden;
margin-top: 0px;
border: 0px solid #dbdbdb;
/* Ensure popups appear above card details on mobile */
z-index: 999999 !important;
/* iOS Safari scrolling fix */
-webkit-overflow-scrolling: touch;
}
.pop-over .header {
color: #fff;
background: #2980b9;
height: 48px;
padding: 0px 0px;
border: 0px;
margin: 0px 0px;
width: 100%;
position: absolute;
top: 0px;
}
.pop-over .header .header-title {
font-size: 20px;
font-weight: normal;
padding-top: 8px;
}
.pop-over .header .back-btn {
width: 30px;
padding: 8px 12px 8px 12px;
}
.pop-over .header .back-btn i.fa {
color: #fff;
}
.pop-over .header .close-btn {
padding: 10px 12px;
}
.pop-over .header .close-btn i.fa {
font-size: 24px;
color: #fff;
}
.pop-over .content-wrapper {
width: 100%;
height: calc(100% - 48px);
overflow-y: scroll;
overflow-x: hidden;
margin: 48px 0px 0px 0px;
}
.pop-over .content-container {
width: 100%;
height: 100%;
max-height: 100%;
}
.pop-over .content-container .content {
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
}
.pop-over .content-container .content form {
margin: 10px 10px;
width: calc(100% - 20px);
}
.pop-over .content-container .content p,
.pop-over .content-container .content textarea,
.pop-over .content-container .content input[type="text"],
.pop-over .content-container .content input[type="email"],
.pop-over .content-container .content input[type="password"],
.pop-over .content-container .content input[type="file"] {
width: 100%;
box-sizing: border-box;
}
.pop-over .pop-over-list li > a {
width: calc(100% - 20px);
margin: 0px 0px;
}
.pop-over .popup-container-depth-1 {
transform: none !important;
}
.pop-over .popup-container-depth-2 {
transform: none !important;
}
.pop-over .popup-container-depth-3 {
transform: none !important;
}
.pop-over .popup-container-depth-4 {
transform: none !important;
}
.pop-over .popup-container-depth-5 {
transform: none !important;
}
.pop-over .popup-container-depth-6 {
transform: none !important;
}
.pop-over .content > form {
padding: 0 1ch;
gap: 0.2lh;
display: flex;
max-width: clamp(20vw, 400px, 50vw);
}
/* Force full-screen popups in mobile mode regardless of screen width */
body.mobile-mode .pop-over {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
max-width: 100vw !important;
max-height: 100vh !important;
}
body.mobile-mode .pop-over .content-wrapper {
width: 100% !important;
height: calc(100vh - 48px) !important;
max-height: calc(100vh - 48px) !important;
overflow-y: auto !important;
overflow-x: hidden !important;
body.mobile-mode .pop-over .content>form {
max-width: 100%;
}
.pop-over .board-subtask-settings {
>h3 {
display: flex;
flex-direction: column;
}
}

View file

@ -1,39 +1,696 @@
Popup.template.events({
'click .js-back-view'() {
Popup.back();
},
'click .js-close-pop-over'() {
Popup.close();
},
'click .js-confirm'() {
this.__afterConfirmAction.call(this);
},
// This handler intends to solve a pretty tricky bug with our popup
// transition. The transition is implemented using a large container
// (.content-container) that is moved on the x-axis (from 0 to n*PopupSize)
// inside a wrapper (.container-wrapper) with a hidden overflow. The problem
// is that sometimes the wrapper is scrolled -- even if there are no
// scrollbars. This happen for instance when the newly opened popup has some
// focused field, the browser will automatically scroll the wrapper, resulting
// in moving the whole popup container outside of the popup wrapper. To
// disable this behavior we have to manually reset the scrollLeft position
// whenever it is modified.
'scroll .content-wrapper'(evt) {
evt.currentTarget.scrollLeft = 0;
},
});
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
import { Template } from 'meteor/templating';
// When a popup content is removed (ie, when the user press the "back" button),
// we need to wait for the container translation to end before removing the
// actual DOM element. For that purpose we use the undocumented `_uihooks` API.
Popup.template.onRendered(() => {
const container = this.find('.content-container');
container._uihooks = {
removeElement(node) {
$(node).addClass('no-height');
$(container).one(CSSEvents.transitionend, () => {
node.parentNode.removeChild(node);
const PopupBias = {
Before: Symbol("S"),
Overlap: Symbol("M"),
After: Symbol("A"),
Fullscreen: Symbol("F"),
includes(e) {
return Object.values(this).includes(e);
}
}
// this class is a bit cumbersome and could probably be done simpler.
// it manages two things : initial placement and sizing given an opener element,
// and then movement and resizing. one difficulty was to be able, as a popup
// which can be resized from the "outside" (CSS4) and move from the inside (inner
// component), which also grows and shrinks frequently, to adapt.
// I tried many approach and failed to get the perfect fit; I feel that there is
// always something indeterminate at some point. so the only drawback is that
// if a popup contains another resizable component (e.g. card details), and if
// it has been resized (with CSS handle), it will lose its dimensions when dragging
// it next time.
class PopupDetachedComponent extends BlazeComponent {
onCreated() {
// Set by parent/caller (usually PopupComponent)
({ nonPlaceholderOpener: this.nonPlaceholderOpener, closeDOMs: this.closeDOMs = [], followDOM: this.followDOM } = this.data());
if (typeof(this.closeDOMs) === "string") {
// helper for passing arg in JADE template
this.closeDOMs = this.closeDOMs.split(';');
}
// The popup's own header, if it exists
this.closeDOMs.push("click .js-close-detached-popup");
}
// Main intent of this component is to have a modular popup with defaults:
// - sticks to its opener while being a child of body (thus in the same stacking context, no z-index issue)
// - is responsive on shrink while keeping position absolute
// - can grow back to initial position step by step
// - exposes various sizes as CSS variables so each rendered popup can use them to adapt defaults
// * issue is that it is done by hand, with heurisitic/simple algorithm from my thoughts, not sure it covers edge cases
// * however it works well so far and maybe more "fixed" element should be popups
onRendered() {
// Remember initial ratio between initial dimensions and viewport
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
this.popup = this.firstNode();
this.popupOpener = this.data().openerElement;
const popupStyle = window.getComputedStyle(this.firstNode());
// margin may be in a relative unit, not computable in JS, but we get the actual pixels here
this.popupMargin = parseFloat(popupStyle.getPropertyValue("--popup-margin"), 10) || Math.min(window.innerWidth / 50, window.innerHeight / 50);
this.dims(this.computeMaxDims());
this.initialPopupWidth = this.popupDims.width;
this.initialPopupHeight = this.popupDims.height;
this.initialHeightRatio = this.initialPopupHeight / viewportHeight;
this.initialWidthRatio = this.initialPopupWidth / viewportWidth;
this.dims(this.computePopupDims());
if (this.followDOM) {
this.innerElement = this.find(this.followDOM) ?? document.querySelector(this.followDOM);
}
this.follow();
this.toFront();
// #FIXME the idea of keeping the initial ratio on resize is quite bad. remove that part.
// there is a reactive variable for window resize in Utils, but the interface is too slow
// with all reactive stuff, use events when possible and when not really bypassing logic
$(window).on('resize', () => {
// #FIXME there is a bug when window grows; popup outer container
// will grow beyond the size of content and it's not easy to fix for me (and I feel tired of this popup)
this.dims(this.computePopupDims());
});
}
margin() {
return this.popupMargin;
}
ensureDimsLimit(dims) {
// boilerplate to make sure that popup visually fits
let { left, top, width, height } = dims;
let overflowBottom = top + height + 2 * this.margin() - window.innerHeight;
let overflowRight = left + width + 2 * this.margin() - window.innerWidth;
if (overflowRight > 0) {
width = Math.max(20 * this.margin(), Math.min(width - overflowRight, window.innerWidth - 2 * this.margin()));
}
if (overflowBottom > 0) {
height = Math.max(10 * this.margin(), Math.min(height - overflowBottom, window.innerHeight - 2 * this.margin()));
}
left = Math.max(left, this.margin());
top = Math.max(top, this.margin());
return { left, top, width, height }
}
dims(newDims) {
if (!this.popupDims) {
this.popupDims = {};
}
if (newDims) {
newDims = this.ensureDimsLimit(newDims);
for (const e of Object.keys(newDims)) {
let value = parseFloat(newDims[e]);
if (!isNaN(value)) {
$(this.popup).css(e, `${value}px`);
this.popupDims[e] = value;
}
}
}
return this.popupDims;
}
isFullscreen() {
return this.fullscreen;
}
maximize() {
this.fullscreen = true;
this.dims(this.computePopupDims());
if (this.innerElement) {
$(this.innerElement).css('width', '');
$(this.innerElement).css('height', '')
}
}
minimize() {
this.fullscreen = false;
this.dims(this.computePopupDims());
}
follow() {
const adaptChild = new ResizeObserver((_) => {
if (this.fullscreen) {return}
const width = this.innerElement?.scrollWidth || this.popup.scrollWidth;
const height = this.innerElement?.scrollHeight || this.popup.scrollHeight;
// we don't want to run this during something that we have caused, eg. dragging
if (!this.mouseDown) {
// extra-"future-proof" stuff: if somebody adds a margin to the popup, it would trigger a loop
if (Math.abs(this.dims().width - width) < 20 && Math.abs(this.dims().height - height) < 20) { return }
// if inner shrinks, follow
if (width < this.dims().width || height < this.dims().height) {
this.dims({ width, height });
}
// otherwise it may be complicated to find a generic situation, but we have the
// classic positionning procedure which works, so use it and ignore positionning
else {
const newDims = this.computePopupDims();
// a bit twisted/ad-hoc for card details, in the edge case where they are opened when collapsed then uncollapsed,
// not sure to understand why the sizing works differently that starting uncollapsed then doing the same sequence
this.dims(this.ensureDimsLimit({
top: this.dims().top,
left: this.dims().left,
width: Math.max(newDims.width, width),
height: Math.max(newDims.height, height)
}));
}
}
else {
const { width, height } = this.popup.getBoundingClientRect();
// only case when we bypass .dims(), to avoid loop
this.popupDims.width = width;
this.popupDims.height = height;
}
});
if (this.innerElement) {
adaptChild.observe(this.innerElement);
} else {
adaptChild.observe(this.popup);
}
}
currentZ(z = undefined) {
// relative, add a constant to be above root elements
if (z !== undefined) {
this.firstNode().style.zIndex = parseInt(z) + 10;
}
return parseInt(this.firstNode().style.zIndex) - 10;
}
// a bit complex...
toFront() {
this.currentZ(Math.max(...PopupComponent.stack.map(p => BlazeComponent.getComponentForElement(p.outerView.firstNode()).currentZ())) || 0 + 1);
}
toBack() {
this.currentZ(Math.min(...PopupComponent.stack.map(p => BlazeComponent.getComponentForElement(p.outerView.firstNode()).currentZ())) || 1 - 1);
}
events() {
// needs to be done at this level; "parent" is not a parent in DOM
let closeEvents = {};
this.closeDOMs?.forEach((e) => {
closeEvents[e] = (_) => {
this.parentComponent().destroy();
}
})
const miscEvents = {
'click .js-confirm'() {
this.data().afterConfirm?.call(this);
},
// bad heuristic but only for best-effort UI
'pointerdown .pop-over'() {
this.mouseDown = true;
},
'pointerup .pop-over'() {
this.mouseDown = false;
}
};
const movePopup = (event) => {
event.preventDefault();
$(event.target).addClass('is-active');
const deltaHandleX = this.dims().left - event.clientX;
const deltaHandleY = this.dims().top - event.clientY;
const onPointerMove = (e) => {
this.dims(this.ensureDimsLimit({ left: e.clientX + deltaHandleX, top: e.clientY + deltaHandleY, width: this.dims().width, height: this.dims().height }));
if (this.popup.scrollY) {
this.popup.scrollTo(0, 0);
}
};
const onPointerUp = (event) => {
$(document).off('pointermove', onPointerMove);
$(document).off('pointerup', onPointerUp);
$(event.target).removeClass('is-active');
};
if (Utils.shouldIgnorePointer(event)) {
onPointerUp(event);
return;
}
$(document).on('pointermove', onPointerMove);
$(document).on('pointerup', onPointerUp);
};
// We do not manage dragging without our own header
const handleDOM = this.data().handleDOM;
if (this.data().showHeader) {
const handleSelector = Utils.isMiniScreen() ? '.js-popup-drag-handle' : '.header-title';
miscEvents[`pointerdown ${handleSelector}`] = (e) => movePopup(e);
}
if (handleDOM) {
miscEvents[`pointerdown ${handleDOM}`] = (e) => movePopup(e);
}
return super.events().concat(closeEvents).concat(miscEvents);
}
computeMaxDims() {
// Get size of inner content, even if it overflows
const content = this.find('.content');
let popupHeight = content.scrollHeight;
let popupWidth = content.scrollWidth;
if (this.data().showHeader) {
const headerRect = this.find('.header');
popupHeight += headerRect.scrollHeight;
popupWidth = Math.max(popupWidth, headerRect.scrollWidth)
}
return { width: Math.max(popupWidth, $(this.popup).width()), height: Math.max(popupHeight, $(this.popup).height()) };
}
placeOnSingleDimension(elementLength, openerPos, openerLength, maxLength, biases, n) {
// avoid too much recursion if no solution
if (!n) {
n = 0;
}
if (n >= 5) {
// if we exhausted a bias, remove it
n = 0;
biases.pop();
if (biases.length === 0) {
return -1;
}
} else {
n += 1;
}
if (!biases?.length) {
const cut = maxLength / 3;
if (openerPos < cut) {
// Corresponds to the default ordering: if element is close to the axe's start,
// try to put the popup after it; then to overlap; and give up otherwise.
biases = [PopupBias.After, PopupBias.Overlap]
}
else if (openerPos > 2 * cut) {
// Same idea if popup is close to the end
biases = [PopupBias.Before, PopupBias.Overlap]
}
else {
// If in the middle, try to overlap: choosing between start or end, even for
// default, is too arbitrary; a custom order can be passed in argument.
biases = [PopupBias.Overlap]
}
}
// Remove the first element and get it
const bias = biases.splice(0, 1)[0];
let factor;
const openerRef = openerPos + openerLength / 2;
if (bias === PopupBias.Before) {
factor = 1;
}
else if (bias === PopupBias.Overlap) {
factor = openerRef / maxLength;
}
else {
factor = 0;
}
let candidatePos = openerRef - elementLength * factor;
const deltaMax = candidatePos + elementLength - maxLength;
if (candidatePos < 0 || deltaMax > 0) {
if (deltaMax <= 2 * this.margin()) {
// if this is just a matter of margin, try again
// useful for (literal) corner cases
biases = [bias].concat(biases);
openerPos -= 5;
}
if (biases.length === 0) {
// we could have returned candidate position even if the size is too large, so
// that the caller can choose, but it means more computations and edge cases...
// any negative means fullscreen overall as the caller will take the maximum between
// margin and candidate.
return -1;
}
return this.placeOnSingleDimension(elementLength, openerPos, openerLength, maxLength, biases, n);
}
return candidatePos;
}
computePopupDims() {
if (!this.isRendered?.()) {
return;
}
// Coordinates of opener related to viewport
let { x: parentX, y: parentY } = this.nonPlaceholderOpener.getBoundingClientRect();
let { height: parentHeight, width: parentWidth } = this.nonPlaceholderOpener.getBoundingClientRect();
// Initial dimensions scaled to the viewport, if it has changed
let popupHeight = window.innerHeight * this.initialHeightRatio;
let popupWidth = window.innerWidth * this.initialWidthRatio;
if (this.fullscreen || Utils.isMiniScreen() && popupWidth >= 4 * window.innerWidth / 5 && popupHeight >= 4 * window.innerHeight / 5) {
// Go fullscreen!
popupWidth = window.innerWidth;
// Avoid address bar, let a bit of margin to scroll
popupHeight = 4 * window.innerHeight / 5;
return ({
width: window.innerWidth,
height: window.innerHeight,
left: 0,
top: 0,
});
},
};
});
} else {
// Current viewport dimensions
let maxHeight = window.innerHeight - this.margin() * 2;
let maxWidth = window.innerWidth - this.margin() * 2;
let biasX, biasY;
if (Utils.isMiniScreen()) {
// On mobile I found that being able to close a popup really close from where it has been clicked
// is comfortable; so given that the close button is top-right, we prefer the position of
// popup being right-bottom, when possible. We then try every position, rather than choosing
// relatively to the relative position of opener in viewport
biasX = [PopupBias.Before, PopupBias.Overlap, PopupBias.After];
biasY = [PopupBias.After, PopupBias.Overlap, PopupBias.Before];
}
const candidateX = this.placeOnSingleDimension(popupWidth, parentX, parentWidth, maxWidth, biasX);
const candidateY = this.placeOnSingleDimension(popupHeight, parentY, parentHeight, maxHeight, biasY);
// Reasonable defaults that can be overriden by CSS later: popups are tall, try to fit the reste
// of the screen starting from parent element, or full screen if element if not fitting
return ({
width: popupWidth,
height: popupHeight,
left: candidateX,
top: candidateY,
});
}
}
}
class PopupComponent extends BlazeComponent {
static stack = [];
// good enough as long as few occurences of such cases
static multipleBlacklist = ["cardDetails"];
// to provide compatibility with Popup.open().
static open(args) {
const openerView = Blaze.getView(args.openerElement);
if (!openerView) {
console.warn(`no parent found for popup ${args.name}, attaching to body: this should not happen`);
}
// render ourselves; everything is automatically managed from that moment, we just added
// a level of indirection but this will not interfere with data
const popup = new PopupComponent();
Blaze.renderWithData(
popup.renderComponent(BlazeComponent.currentComponent()),
args,
args.openerElement,
null,
openerView
);
return popup;
}
static destroy() {
PopupComponent.stack.at(-1)?.destroy();
}
static findParentPopup(element) {
return BlazeComponent.getComponentForElement($(element).closest('.pop-over')[0]);
}
static toFront(event) {
const popup = PopupComponent.findParentPopup(event.target)
popup?.toFront();
return popup;
}
static toBack(event) {
const popup = PopupComponent.findParentPopup(event.target);
popup?.toBack();
return popup;
}
static maximize(event) {
const popup = PopupComponent.findParentPopup(event.target);
popup?.toFront();
popup?.maximize();
return popup;
}
static minimize(event) {
const popup = PopupComponent.findParentPopup(event.target);
popup?.minimize();
return popup;
}
getOpenerElement(view) {
// Look for the first parent view whose first DOM element is not virtually us
const firstNode = $(view.firstNode());
// The goal is to have the best chances to get the element whose size and pos
// are relevant; e.g. when clicking on a date on a minicard, we don't wan't
// the opener to be set to the minicard.
// In order to work in general, we need to take special situations into account,
// e.g. the placeholder is isolated, or does not have previous node, and so on.
// In general we prefer previous node, then next, then any displayed sibling,
// then the parent, and so on.
let candidates = [];
if (!firstNode.hasClass(this.popupPlaceholderClass())) {
candidates.push(firstNode);
}
candidates = candidates.concat([firstNode.prev(), firstNode.next()]);
const otherSiblings = Array.from(firstNode.siblings()).filter(e => !candidates.includes(e));
for (const cand of candidates.concat(otherSiblings)) {
const displayCSS = cand?.css("display");
if (displayCSS && displayCSS !== "none") {
return cand[0];
}
}
return this.getOpenerElement(view.parentView);
}
getParentData(view) {;
let data;
// ⚠️ node can be a text node
while (view.firstNode?.()?.classList?.contains(this.popupPlaceholderClass())) {
view = view.parentView;
data = Blaze.getData(view);
}
// This is VERY IMPORTANT to get data like this and not with templateInstance.data,
// because this form is reactive. So all inner popups have reactive data, which is nice
return data;
}
onCreated() {
// #FIXME prevent secondary popups to open
// Special "magic number" case: never render, for any reason, the same card
// const maybeID = this.parentComponent?.()?.data?.()?._id;
// if (maybeID && PopupComponent.stack.find(e => e.parentComponent().data?.()?._id === maybeID)) {
// this.destroy();
// return;
// }
// do not render a template multiple times
const existing = PopupComponent.stack.find((e) => (e.name == this.data().name));
if (existing && PopupComponent.multipleBlacklist.indexOf(this.data().name)) {
// ⚠️ is there a default better than another? I feel that closing existing
// popup is not bad in general because having the same button for open and close
// is common
if (PopupComponent.multipleBlacklist.includes(existing.name)) {
existing.destroy();
}
// but is could also be re-rendering, eg
// existing.render();
return;
}
// All of this, except name, is optional. The rest is provided "just in case", for convenience (hopefully)
//
// - name is the name of a template to render inside the popup (to the detriment of its size) or the contrary
// - showHeader can be turned off if the inner content always have a header with buttons and so on
// - title is shown when header is shown
// - miscOptions is for compatibility
// - closeVar is an optional string representing a Session variable: if set, the popup reactively closes when the variable changes and set the variable to null on close
// - closeDOMs can be used alternatively; it is an array of "<event> <selector>" to listen that closes the popup.
// if header is shown, closing the popup is already managed. selector is relative to the inner template (same as its event map)
// - followDOM is an element whose dimension will serve as reference so that popup can react to inner changes; works only with inline styles (otherwise we probably would need IntersectionObserver-like stuff, async etc)
// - handleDOM is an element who can be clicked to move popup
// it is useful when the content can be redimensionned/moved by code or user; we still manage events, resizes etc
// but allow inner elements or handles to do it (and we adapt).
const data = this.data();
this.popupArgs = {
name: data.name,
showHeader: data.showHeader ?? true,
title: data.title,
openerElement: data.openerElement,
closeDOMs: data.closeDOMs,
followDOM: data.followDOM,
handleDOM: data.handleDOM,
forceData: data.miscOptions?.dataContextIfCurrentDataIsUndefined,
afterConfirm: data.miscOptions?.afterConfirm,
}
this.name = this.data().name;
this.innerTemplate = Template[this.name];
this.innerComponent = BlazeComponent.getComponent(this.name);
this.outerComponent = BlazeComponent.getComponent('popupDetached');
if (!(this.innerComponent || this.innerTemplate)) {
throw new Error(`template and/or component ${this.name} not found`);
}
// If arg is not set, must be closed manually by calling destroy()
if (this.popupArgs.closeVar) {
this.closeInitialValue = Session.get(this.data().closeVar);
if (!this.closeInitialValue === undefined) {
this.autorun(() => {
if (Session.get(this.data().closeVar) !== this.closeInitialValue) {
this.onDestroyed();
}
});
}
}
}
popupPlaceholderClass() {
return "popup-placeholder";
}
render() {
const oldOuterView = this.outerView;
// see below for comments
this.outerView = Blaze.renderWithData(
// data is passed through the parent relationship
// we need to render it again to keep events in sync with inner popup
this.outerComponent.renderComponent(this.component()),
this.popupArgs,
document.body,
null,
this.openerView
);
this.innerView = Blaze.renderWithData(
// the template to render: either the content is a BlazeComponent or a regular template
// if a BlazeComponent, render it as a template first
this.innerComponent?.renderComponent?.(this.component()) || this.innerTemplate,
// dataContext used for rendering: each time we go find data, because it is non-reactive
() => (this.popupArgs.forceData || this.getParentData(this.currentView)),
// DOM parent: ask to the detached popup, will be inserted at the last child
this.outerView.firstNode()?.getElementsByClassName('content')?.[0] || document.body,
// "stop" DOM element; we don't use
null,
// important: this is the Blaze.View object which will be set as `parentView` of
// the rendered view. we set it as the parent view, so that the detached popup
// can interact with its "parent" without being a child of it, and without
// manipulating DOM directly.
this.openerView
);
if (oldOuterView) {
Blaze.remove(oldOuterView);
}
}
onRendered() {
if (this.detached) {return}
// Use plain Blaze stuff to be able to render all templates, but use components when available/relevant
this.currentView = Blaze.currentView || Blaze.getView(this.component().firstNode());
// Placement will be related to the opener (usually clicked element)
// But template data and view related to the opener are not the same:
// - view is probably outer, as is was already rendered on click
// - template data could be found with Template.parentData(n), but `n` can
// vary depending on context: using those methods feels more reliable for this use case
this.popupArgs.openerElement ??= this.getOpenerElement(this.currentView);
this.openerView = Blaze.getView(this.popupArgs.openerElement);
// With programmatic/click opening, we get the "real" opener; with dynamic
// templating we get the placeholder and need to go up to get a glimpse of
// the "real" opener size. It is quite imprecise in that case (maybe the
// interesting opener is a sibling, not an ancestor), but seems to do the job
// for now.
// Also it feels sane that inner content does not have a reference to
// a virtual placeholder.
const opener = this.popupArgs.openerElement;
let sizedOpener = opener;
if (opener.classList?.contains?.(this.popupPlaceholderClass())) {
sizedOpener = opener.parentNode;
}
this.popupArgs.nonPlaceholderOpener = sizedOpener;
PopupComponent.stack.push(this);
try {
this.render();
// Render above other popups by default
} catch(e) {
// If something went wrong during rendering, do not create
// "zombie" popups
console.error(`cannot render popup ${this.name}: ${e}`);
this.destroy();
}
}
destroy() {
this.detached = true;
if (!PopupComponent.stack.includes(this)) {
// Avoid loop destroy
return;
}
// Maybe overkill but may help to avoid leaking memory
// as programmatic rendering is less usual
for (const view of [this.innerView, this.currentView, this.outerView]) {
try {
Blaze.remove(view);
} catch {
console.warn(`A view failed to be removed: ${view}`)
}
}
this.innerComponent?.removeComponent?.();
this.outerComponent?.removeComponent?.();
this.removeComponent();
// not necesserly removed in order, e.g. multiple cards
PopupComponent.stack = PopupComponent.stack.filter(e => e !== this);
}
closeWithPlaceholder(parentElement) {
// adapted from https://stackoverflow.com/questions/52834774/dom-event-when-element-is-removed
// strangely, when opener is removed because of a reactive change, this component
// do not get any lifecycle hook called, so we need to bridge the gap. Simply
// "close" popup when placeholder is off-DOM.
while (parentElement.nodeType === Node.TEXT_NODE) {
parentElement = parentElement.parentElement;
}
const placeholder = parentElement.getElementsByClassName(this.popupPlaceholderClass());
if (!placeholder.length) {
return;
}
const observer = new MutationObserver(() => {
// DOM element being suppressed is reflected in array
if (placeholder.length === 0) {
this.destroy();
}
});
observer.observe(parentElement, {childList: true});
}
}
PopupComponent.register("popup");
PopupDetachedComponent.register('popupDetached');
export default PopupComponent;

View file

@ -1,24 +0,0 @@
.pop-over.js-pop-over(
class="{{#unless title}}miniprofile{{/unless}}"
class=currentBoard.colorClass
class="{{#unless title}}no-title{{/unless}}"
data-popup="{{popupName}}"
style="left:{{offset.left}}px; top:{{offset.top}}px;{{#if offset.maxHeight}} max-height:{{offset.maxHeight}}px;{{/if}}")
.header
a.back-btn.js-back-view(class="{{#unless hasPopupParent}}is-hidden{{/unless}}")
i.fa.fa-caret-left
span.header-title= title
a.close-btn.js-close-pop-over
i.fa.fa-times-thin
.content-wrapper
//-
We display the all stack of popup content next to each other and move
the "window" by translating .content-container inside .content-wrapper.
.content-container(class="popup-container-depth-{{depth}}")
each stack
//-
XXX We need a better way to express the "is the last element" condition.
Hopefully the @last helper will come soon (or at least @index)
.content(class="{{#unless $eq popupName ../popupName}}no-height{{/unless}}")
+Template.dynamic(template=popupName data=dataContext)
.clearfix

View file

@ -3,7 +3,7 @@
height: 50px;
margin: auto;
text-align: center;
font-size: 10px;
}
.sk-spinner-wave div {
background-color: #333;

View file

@ -1,17 +1,40 @@
#notifications {
position: relative;
}
#notifications .notifications-drawer-toggle {
display: block;
line-height: 28px;
color: #f2f2f2;
margin: 0 10px;
width: 28px;
height: 28px;
text-align: center;
border: 0;
padding: 0;
.notifications-container {
/* absolute to render close to emoji and render on top,
"naturally" on top because no parent stacking context */
position: absolute;
right: 0;
top: 1.5lh;
background-color: #fafafa;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
border-radius: 2px;
color: #000;
z-index: 1;
}
#notifications .notifications-drawer-toggle.alert {
background-color: #eb4646;
}
#notifications {
/* to position popup */
position: relative;
overflow: visible;
}
#notifications-drawer {
position: relative;
min-height: min-content;
height: fit-content;
max-height: 100vh;
z-index: 300;
width: max-content;
.fa {
color: #bcbcbc !important;
}
}
body.mobile-mode {
#notifications-drawer .header {
flex-direction: column;
}
}

View file

@ -1,6 +1,7 @@
template(name='notifications')
#notifications.board-header-btns.right
.notifications-container
if $.Session.get 'showNotificationsDrawer'
+notificationsDrawer(unreadNotifications=unreadNotifications)
a.notifications-drawer-toggle(class="{{#if $gt unreadNotifications 0}}alert{{/if}}" title="{{_ 'notifications'}}")
i.fa.fa-bell
if $.Session.get 'showNotificationsDrawer'
+notificationsDrawer(unreadNotifications=unreadNotifications)

View file

@ -1,38 +1,16 @@
section#notifications-drawer {
position: fixed;
top: 48px;
right: 0;
width: 400px;
background-color: #fafafa;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
border-radius: 2px;
max-height: calc(100vh - 28px - 36px);
color: #000;
padding-top: 36px;
}
section#notifications-drawer a:hover {
color: #2980b9 !important;
}
section#notifications-drawer .header {
position: fixed;
top: 48px;
right: 0;
width: calc(400px - 32px);
padding: 8px 16px;
section#notifications-drawer .header {
display: flex;
justify-content: space-between;
padding: 0.5lh 2ch;
gap: 0.5lh;
align-items: center;
background: #ededed;
border-bottom: 1px solid #dbdbdb;
z-index: 2;
}
section#notifications-drawer .header .notification-menu-toggle {
position: absolute;
left: 16px;
top: calc(50% - 12px);
font-size: 20px;
cursor: pointer;
color: #333;
line-height: 24px;
}
section#notifications-drawer .header .notification-menu-toggle:hover {
section#notifications-drawer .header .toggle-read {
color: #2980b9;
}
section#notifications-drawer .header .notification-menu {
@ -88,19 +66,13 @@ section#notifications-drawer .header h5 {
margin: 0;
}
section#notifications-drawer .header .close {
position: absolute;
top: calc(50% - 12px);
right: 12px;
font-size: 24px;
height: 24px;
line-height: 24px;
display: flex;
opacity: 1;
}
section#notifications-drawer ul.notifications {
display: block;
padding: 0px 16px 0px 16px;
margin: 0;
height: calc(100vh - 122px);
overflow-y: scroll;
height: fit-content;
display: flex;
flex-direction: column;
}

View file

@ -3,6 +3,7 @@ template(name='notificationsDrawer')
.header
a.notification-menu-toggle
i.fa.fa-bars
//- #FIXME could be replaced by a popup to help placement ?
.notification-menu(class="{{#if $.Session.get 'showNotificationMenu'}}is-open{{/if}}")
.menu-section
a.menu-item(class="{{#unless $.Session.get 'showReadNotifications'}}selected{{/unless}}")
@ -44,9 +45,10 @@ template(name='notificationsDrawer')
span.menu-icon
i.fa.fa-trash
span {{_ 'delete-all-notifications'}}
h5 {{_ 'notifications'}}
if($gt unreadNotifications 0)
|(#{unreadNotifications})
if($gt unreadNotifications 0)
|(#{unreadNotifications}) {{_ 'notifications'}}
else
|0 {{_ 'notifications'}}
a.close
i.fa.fa-times-thin
ul.notifications

View file

@ -85,4 +85,5 @@ template(name="setCardActionsColorPopup")
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
if(isSelected color)
i.fa.fa-check
button.primary.confirm.js-submit {{_ 'save'}}
.form-buttons
button.primary.confirm.js-submit {{_ 'save'}}

View file

@ -80,7 +80,7 @@
.triggers-content .triggers-body .triggers-side-menu {
background-color: #f7f7f7;
border: 1px solid #f0f0f0;
border-radius: 4px;
border-radius: 0.4ch;
height: intrinsic;
box-shadow: inset -1px -1px 3px rgba(0,0,0,0.05);
}
@ -89,7 +89,7 @@
width: 50px;
height: 50px;
text-align: center;
font-size: 25px;
position: relative;
}
.triggers-content .triggers-body .triggers-side-menu ul li i {
@ -112,7 +112,7 @@
width: 95%;
}
.triggers-content .triggers-body .triggers-side-menu ul li a span {
font-size: 13px;
}
.triggers-content .triggers-body .triggers-main-body {
padding: 0.1em 1em;
@ -134,15 +134,15 @@
left: 10px;
}
.triggers-content .triggers-body .triggers-main-body .trigger-item .trigger-content .trigger-text {
font-size: 16px;
display: inline-block;
}
.triggers-content .triggers-body .triggers-main-body .trigger-item .trigger-content .trigger-inline-button {
font-size: 16px;
display: inline;
padding: 6px;
border: 1px solid #eee;
border-radius: 4px;
border-radius: 0.4ch;
box-shadow: inset -1px -1px 3px rgba(0,0,0,0.05);
}
.triggers-content .triggers-body .triggers-main-body .trigger-item .trigger-content .trigger-inline-button:hover,
@ -179,10 +179,10 @@
width: 30px;
height: 30px;
border: 1px solid #eee;
border-radius: 4px;
border-radius: 0.4ch;
box-shadow: inset -1px -1px 3px rgba(0,0,0,0.05);
text-align: center;
font-size: 20px;
right: 10px;
}
.triggers-content .triggers-body .triggers-main-body .trigger-item .trigger-button i {
@ -206,7 +206,7 @@
top: unset;
position: unset;
transform: unset;
font-size: 16px;
width: auto;
padding-left: 10px;
padding-right: 10px;

View file

@ -19,7 +19,7 @@
.migration-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
@ -35,8 +35,8 @@
.migration-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border-radius: 0.4ch;
border: none;
cursor: pointer;
transition: all 0.3s ease;
@ -72,7 +72,7 @@
.migration-progress {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-radius: 0.8ch;
margin-bottom: 30px;
border-left: 4px solid #667eea;
}
@ -128,20 +128,20 @@
text-align: center;
font-weight: 700;
color: #667eea;
font-size: 18px;
}
.progress-label {
text-align: center;
color: #666;
font-size: 14px;
margin-top: 4px;
}
.current-step {
text-align: center;
color: #333;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
@ -154,7 +154,7 @@
.migration-status {
text-align: center;
color: #333;
font-size: 16px;
background-color: #e3f2fd;
padding: 12px 16px;
border-radius: 6px;
@ -173,7 +173,7 @@
.migration-steps h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 20px;
font-weight: 600;
}
@ -181,7 +181,7 @@
max-height: 400px;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
border-radius: 0.8ch;
}
.migration-step {
@ -210,7 +210,7 @@
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(102, 126, 234, 0);
box-shadow: 0 0 0 0.5rem rgba(102, 126, 234, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0);
@ -225,7 +225,7 @@
.step-icon {
margin-right: 12px;
font-size: 18px;
width: 24px;
text-align: center;
}
@ -249,13 +249,13 @@
.step-name {
font-weight: 600;
color: #333;
font-size: 14px;
margin-bottom: 2px;
}
.step-description {
color: #666;
font-size: 12px;
line-height: 1.3;
}
@ -265,7 +265,7 @@
}
.step-progress .progress-text {
font-size: 12px;
font-weight: 600;
}
@ -302,7 +302,7 @@
.jobs-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
@ -313,8 +313,8 @@
.jobs-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border-radius: 0.4ch;
border: none;
cursor: pointer;
transition: all 0.3s ease;
@ -337,7 +337,7 @@
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 8px;
border-radius: 0.8ch;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@ -356,18 +356,18 @@
.table th {
font-weight: 600;
color: #333;
font-size: 14px;
}
.table td {
font-size: 14px;
color: #666;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
border-radius: 0.4ch;
font-weight: 600;
text-transform: uppercase;
}
@ -404,7 +404,7 @@
.btn-group .btn {
padding: 4px 8px;
font-size: 12px;
border-radius: 3px;
border: none;
cursor: pointer;
@ -452,7 +452,7 @@
.add-job-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
@ -474,15 +474,15 @@
margin-bottom: 8px;
font-weight: 600;
color: #333;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
border-radius: 0.4ch;
transition: border-color 0.3s ease;
}
@ -504,8 +504,8 @@
.form-actions .btn {
padding: 10px 20px;
font-size: 14px;
border-radius: 4px;
border-radius: 0.4ch;
border: none;
cursor: pointer;
transition: all 0.3s ease;
@ -546,7 +546,7 @@
.board-operations-header h2 {
margin: 0;
color: #333;
font-size: 24px;
font-weight: 600;
}
@ -562,8 +562,8 @@
.board-operations-controls .btn {
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border-radius: 0.4ch;
border: none;
cursor: pointer;
transition: all 0.3s ease;
@ -590,7 +590,7 @@
.board-operations-stats {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-radius: 0.8ch;
margin-bottom: 30px;
border-left: 4px solid #667eea;
}
@ -606,14 +606,14 @@
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #667eea;
margin-bottom: 4px;
}
.stat-label {
font-size: 14px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
@ -622,7 +622,7 @@
.system-resources {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-radius: 0.8ch;
margin-bottom: 30px;
border-left: 4px solid #28a745;
}
@ -641,7 +641,7 @@
min-width: 120px;
font-weight: 600;
color: #333;
font-size: 14px;
}
.resource-bar {
@ -674,7 +674,7 @@
text-align: right;
font-weight: 600;
color: #333;
font-size: 14px;
}
.board-operations-search {
@ -683,7 +683,7 @@
.search-box {
position: relative;
max-width: 400px;
max-width: 50vw;
}
.search-box .form-control {
@ -696,12 +696,12 @@
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 16px;
}
.board-operations-list {
background: white;
border-radius: 8px;
border-radius: 0.8ch;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
@ -718,13 +718,13 @@
.operations-header h3 {
margin: 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
.pagination-info {
color: #666;
font-size: 14px;
}
.operations-table {
@ -751,11 +751,11 @@
.board-id {
font-family: monospace;
font-size: 12px;
color: #666;
background: #f8f9fa;
padding: 4px 8px;
border-radius: 4px;
border-radius: 0.4ch;
display: inline-block;
}
@ -776,19 +776,19 @@
flex: 1;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
border-radius: 0.4ch;
overflow: hidden;
}
.progress-container .progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 4px;
border-radius: 0.4ch;
transition: width 0.3s ease;
}
.progress-container .progress-text {
font-size: 12px;
font-weight: 600;
color: #667eea;
min-width: 35px;
@ -806,8 +806,8 @@
.pagination .btn {
padding: 6px 12px;
font-size: 12px;
border-radius: 4px;
border-radius: 0.4ch;
border: 1px solid #ddd;
background: white;
color: #333;
@ -827,7 +827,7 @@
.page-info {
color: #666;
font-size: 14px;
}
/* Responsive design */
@ -838,26 +838,26 @@
align-items: flex-start;
gap: 15px;
}
.migration-controls,
.jobs-controls {
width: 100%;
justify-content: center;
}
.table {
font-size: 12px;
}
.table th,
.table td {
padding: 8px 12px;
}
.btn-group {
flex-direction: column;
}
.add-job-form {
max-width: 100%;
}
@ -878,7 +878,7 @@
#cron-setting .progress {
height: 30px;
background-color: #e9ecef;
border-radius: 4px;
border-radius: 0.4ch;
overflow: visible;
margin-bottom: 5px;
max-width: calc(100% - 40px);
@ -893,7 +893,7 @@
font-size: 14px;
text-align: center;
transition: width 0.3s ease;
border-radius: 4px;
border-radius: 0.4ch;
}
#cron-setting .progress-text {

View file

@ -15,7 +15,7 @@
.migration-progress-modal {
background: white;
border-radius: 8px;
border-radius: 0.8ch;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
@ -46,13 +46,13 @@
.migration-progress-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.migration-progress-close {
cursor: pointer;
font-size: 16px;
opacity: 0.8;
transition: opacity 0.2s ease;
}
@ -73,7 +73,7 @@
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.migration-progress-overall-bar {
@ -110,7 +110,7 @@
.migration-progress-overall-percentage {
text-align: right;
font-size: 12px;
color: #666;
font-weight: 600;
}
@ -123,12 +123,12 @@
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.migration-progress-step-bar {
background: #e9ecef;
border-radius: 8px;
border-radius: 0.8ch;
height: 8px;
overflow: hidden;
margin-bottom: 5px;
@ -137,13 +137,13 @@
.migration-progress-step-fill {
background: linear-gradient(90deg, #007bff, #0056b3);
height: 100%;
border-radius: 8px;
border-radius: 0.8ch;
transition: width 0.3s ease;
}
.migration-progress-step-percentage {
text-align: right;
font-size: 12px;
color: #666;
font-weight: 600;
}
@ -160,12 +160,12 @@
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 13px;
}
.migration-progress-status-text {
color: #555;
font-size: 14px;
line-height: 1.4;
}
@ -181,12 +181,12 @@
font-weight: 600;
color: #1976d2;
margin-bottom: 5px;
font-size: 13px;
}
.migration-progress-details-text {
color: #1565c0;
font-size: 13px;
line-height: 1.4;
}
@ -199,7 +199,7 @@
.migration-progress-note {
text-align: center;
color: #666;
font-size: 13px;
font-style: italic;
}
@ -209,17 +209,17 @@
width: 95%;
margin: 20px;
}
.migration-progress-content {
padding: 20px;
}
.migration-progress-header {
padding: 15px;
}
.migration-progress-title {
font-size: 16px;
}
}
@ -229,40 +229,40 @@
background: #2d3748;
color: #e2e8f0;
}
.migration-progress-overall-label,
.migration-progress-step-label,
.migration-progress-status-label {
color: #e2e8f0;
}
.migration-progress-status {
background: #4a5568;
border-left-color: #63b3ed;
}
.migration-progress-status-text {
color: #cbd5e0;
}
.migration-progress-details {
background: #2b6cb0;
border-left-color: #4299e1;
}
.migration-progress-details-label {
color: #bee3f8;
}
.migration-progress-details-text {
color: #90cdf4;
}
.migration-progress-footer {
background: #4a5568;
border-top-color: #718096;
}
.migration-progress-note {
color: #a0aec0;
}
@ -285,7 +285,7 @@
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 4px;
border-radius: 0.4ch;
}
.migration-spinner {

View file

@ -39,9 +39,6 @@ table tr:nth-child(even) {
.ext-box button {
min-width: 90px;
}
.content-wrapper {
margin-top: 10px;
}
.buttonsContainer {
display: flex;
}
@ -164,7 +161,7 @@ table td:first-child {
background-color: #27ae60;
color: white;
padding: 10px 20px;
border-radius: 4px;
border-radius: 0.4ch;
z-index: 9999;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
animation: fadeOut 3s ease-in forwards;

View file

@ -9,19 +9,32 @@
display: flex;
height: 100%;
}
.setting-detail {
display: flex;
flex-direction: column;
flex: 1;
justify-content: stretch;
align-items: stretch;
}
.setting-content {
color: #727479;
background: #dedede;
width: 100%;
height: 100%;
position: absolute;
overflow-y: scroll;
}
.setting-content .wekan-form-control:not([type="radio"]) {
display: flex;
width: 100%;
}
.setting-content .content-title {
font-size: clamp(16px, 3.5vw, 22px);
font-size: 1.3em;
padding: 0.5lh 1ch;
}
.setting-content .content-body {
display: flex;
padding-top: 2vh;
height: 100%;
gap: 1.3vw;
}
@ -29,8 +42,15 @@
background-color: #f7f7f7;
border: 1px solid #f0f0f0;
border-radius: 0.5vw;
width: min(250px, 32vw);
min-width: fit-content;
box-shadow: inset -0.2vh -0.2vh 0.4vh rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
padding-right: 2ch;
overflow-y: scroll;
min-height: 20vh;
flex-grow: 1;
}
.setting-content .content-body .side-menu ul li {
margin: 0.2vh 0.3vw;
@ -47,12 +67,10 @@
padding: 1.3vh 0 1.3vh 1.3vw;
width: 95%;
}
.setting-content .content-body .side-menu ul li a span {
font-size: 13px;
}
.setting-content .content-body .side-menu ul li a i {
margin-right: 20px;
}
.setting-content .content-body .main-body {
-webkit-user-select: text;
-moz-user-select: text;
@ -62,9 +80,9 @@
overflow-x: scroll !important;
overflow-y: scroll !important;
scrollbar-gutter: stable;
/* Force horizontal scrollbar to always be visible */
min-width: 100%;
width: 100%;
flex-grow: 5;
padding-right: 2ch;
padding-bottom: 1lh;
}
/* Ensure scrollbars are always visible with proper styling for all admin pages */
@ -126,7 +144,6 @@
.setting-content .content-body .main-body::after {
content: '';
display: block;
width: 100vw;
height: 1px;
position: absolute;
bottom: 0;
@ -137,7 +154,7 @@
padding: 0.5rem 0.5rem;
}
.setting-content .content-body .main-body ul li a .is-checked {
border-bottom: 2px solid #3cb500;
border-bottom: 0.2ch solid #3cb500;
border-right: 2px solid #3cb500;
}
/* Grey checkmarks when grey icons setting is enabled */

View file

@ -4,7 +4,7 @@
margin-left: 20px;
padding-right: 10px;
height: 28px;
font-size: 13px;
float: left;
overflow: hidden;
line-height: 28px;
@ -26,3 +26,12 @@
margin-top: 1px;
margin-right: 10px;
}
.setting-header-btns {
display: flex;
align-items: center;
gap: 1ch;
padding: 0 1ch;
flex-wrap: wrap;
}

View file

@ -32,9 +32,6 @@ table tr:nth-child(even) {
.ext-box button {
min-width: 90px;
}
.content-wrapper {
margin-top: 10px;
}
.buttonsContainer {
display: flex;
}

View file

@ -13,7 +13,7 @@
position: absolute;
right: 0px;
top: 0px;
font-size: 25px;
padding: 10px;
}
.sidebar-xmark:hover {
@ -27,7 +27,21 @@
padding: 10px 10px 0px 10px;
}
.sidebar .sidebar-content {
padding: 0 12px;
padding: 0 1ch;
>ul {
display: flex;
}
.fa:not(.fa-plus) {
padding-right: 0.5ch;
align-self: center;
}
*:has(>.fa-plus) {
/* as long as container as a min height,
we can accomodate it while staying symetric */
aspect-ratio: 1/1;
height: var(--label-height);
min-width: 0;
}
}
.sidebar .sidebar-content .hide-btn {
display: none;
@ -38,15 +52,13 @@
margin-bottom: 10px;
font-weight: bold;
}
.sidebar .sidebar-content h3 i.fa {
margin-right: 3px;
}
.sidebar .sidebar-content hr {
margin: 13px 0;
}
.sidebar .sidebar-content ul.sidebar-list {
display: flex;
flex-direction: column;
gap: 0.1lh;
}
/* Use checklist-style green checkboxes for all sidebar checkboxes */
@ -60,7 +72,7 @@
margin-right: 6px !important;
border-top: 2px solid transparent !important;
border-left: 2px solid transparent !important;
border-bottom: 2px solid #3cb500 !important;
border-bottom: 0.2ch solid #3cb500 !important;
border-right: 2px solid #3cb500 !important;
transform: rotate(40deg) !important;
-webkit-backface-visibility: hidden !important;
@ -105,16 +117,17 @@ body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked
.card-settings-column h4 {
margin: 0;
font-size: 12px;
font-weight: bold;
text-align: center;
}
.sidebar .sidebar-content ul.sidebar-list li > a {
display: flex;
height: 30px;
margin: 0;
padding: 4px;
border-radius: 3px;
max-height: 2lh;
overflow: hidden;
align-items: center;
}
.sidebar .sidebar-content ul.sidebar-list li > a:hover,
@ -132,10 +145,6 @@ body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked
padding: 8px;
border-radius: 3px;
}
.sidebar .sidebar-content ul.sidebar-list li > a .sidebar-list-item-description {
flex: 1;
overflow: ellipsis;
}
.sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
margin: 0 4px;
color: #3cb500;
@ -144,9 +153,6 @@ body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked
body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
color: #7a7a7a;
}
.sidebar .sidebar-content ul.sidebar-list li .minicard {
padding: 6px 8px 4px;
}
.sidebar .sidebar-content ul.sidebar-list li .minicard .minicard-edit-button {
float: right;
padding: 4px;
@ -183,13 +189,28 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
}
.board-sidebar {
display: none;
width: 30vw;
z-index: 100;
width: fit-content;
height: fit-content;
max-width: min(50ch, 50vw);
max-height: 100vh;
overflow: auto;
z-index: 10;
transition: top 0.1s, right 0.1s, width 0.1s;
}
body.mobile-mode .board-sidebar {
max-width: 100vw;
}
.board-sidebar.is-open {
display: block;
}
.board-widget-content {
display: flex;
flex-wrap: wrap;
gap: 0.2lh;
min-height: 1.5lh;
align-items: stretch;
}
.board-widget h4 {
margin: 5px 0;
}
@ -212,7 +233,7 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
}
.sidebar-tongue i.fa {
padding: 3px 9px;
font-size: 24px;
transition: transform 0.5s;
}
.sidebar-accessibility {
@ -283,7 +304,7 @@ body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-
}
.board-sidebar .sidebar-content .hide-btn i.fa {
padding: 8px 16px;
font-size: 24px;
font-weight: bold;
}
.sidebar-tongue {

View file

@ -48,8 +48,9 @@ template(name='homeSidebar')
hr
if currentUser.isBoardAdmin
h3.activity-title
i.fa.fa-comment-o
| {{_ 'activities'}}
span
i.fa.fa-comment-o
span {{_ 'activities'}}
a.flex.js-toggle-show-activities(title="{{_ 'show-activities'}}")
i.fa(class="{{#if showActivities}}fa-check{{else}}fa-square-o{{/if}}")
@ -60,7 +61,7 @@ template(name="membersWidget")
unless currentUser.isCommentOnly
unless currentUser.isWorker
h3
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
a.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}")
i.fa.fa-cog
| {{_ 'boardMenuPopup-title'}}
hr
@ -161,7 +162,7 @@ template(name="boardChangeBackgroundImagePopup")
form
label
| {{_ 'board-background-image-url'}}
input.js-board-background-image-url(type="text" value="{{backgroundImageURL}}" autofocus)
input.js-board-background-image-url(type="text" value="{{backgroundImageURL}}" )
div.buttonsContainer
input.primary.wide(type="submit" value="{{_ 'save'}}")
br
@ -307,7 +308,7 @@ template(name="boardCardSettingsPopup")
.card-settings-column
span
i.fa.fa-user
|
i.fa.fa-plus
| {{_ 'requested-by'}}
.card-settings-row
.card-settings-column
@ -635,6 +636,10 @@ template(name="boardMenuPopup")
a.js-archive-board
i.fa.fa-archive
| {{_ 'archive-board'}}
//- this popup is the only one to not open
//- with correct size; related to issue linked above ?
//- artificially add a bit a space
div.invisible-line
template(name="exportBoard")
ul.pop-over-list

View file

@ -41,16 +41,15 @@ BlazeComponent.extendComponent({
},
open() {
if (!this._isOpen.get()) {
this._isOpen.set(true);
EscapeActions.executeUpTo('detailsPane');
}
// setting a ReactiveVar is idempotent;
// do not try to get(), because it will
// react to changes...
this._isOpen.set(true);
EscapeActions.executeUpTo('detailsPane');
},
hide() {
if (this._isOpen.get()) {
this._isOpen.set(false);
}
this._isOpen.set(false);
},
toggle() {
@ -154,7 +153,7 @@ BlazeComponent.extendComponent({
ReactiveCache.getCurrentUser().toggleVerticalScrollbars();
},
'click .js-show-week-of-year-toggle'() {
ReactiveCache.getCurrentUser().toggleShowWeekOfYear();
Meteor.call('toggleShowWeekOfYear');
},
'click .sidebar-accessibility'() {
FlowRouter.go('accessibility');
@ -291,10 +290,10 @@ Template.boardMenuPopup.events({
'click .js-delete-duplicate-lists': Popup.afterConfirm('deleteDuplicateLists', function() {
const currentBoard = Utils.getCurrentBoard();
if (!currentBoard) return;
// Get all lists in the current board
const allLists = ReactiveCache.getLists({ boardId: currentBoard._id, archived: false });
// Group lists by title to find duplicates
const listsByTitle = {};
allLists.forEach(list => {
@ -303,7 +302,7 @@ Template.boardMenuPopup.events({
}
listsByTitle[list.title].push(list);
});
// Find and delete duplicate lists that have no cards
let deletedCount = 0;
Object.keys(listsByTitle).forEach(title => {
@ -313,7 +312,7 @@ Template.boardMenuPopup.events({
for (let i = 1; i < listsWithSameTitle.length; i++) {
const list = listsWithSameTitle[i];
const cardsInList = ReactiveCache.getCards({ listId: list._id, archived: false });
if (cardsInList.length === 0) {
Lists.remove(list._id);
deletedCount++;
@ -321,7 +320,7 @@ Template.boardMenuPopup.events({
}
}
});
// Show notification
if (deletedCount > 0) {
// You could add a toast notification here if available
@ -402,7 +401,7 @@ Template.memberPopup.events({
FlowRouter.go('home');
});
}),
});
Template.removeMemberPopup.helpers({
@ -934,7 +933,7 @@ BlazeComponent.extendComponent({
// Get the current board reactively using board ID from Session
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
let result = currentBoard ? currentBoard.presentParentTask : null;
if (result === null || result === undefined) {
result = 'no-parent';
@ -947,7 +946,7 @@ BlazeComponent.extendComponent({
{
'click .js-field-has-subtasks'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsSubtasks;
const newValue = !this.allowsSubtasks();
Boards.update(this.currentBoard._id, { $set: { allowsSubtasks: newValue } });
$('.js-field-deposit-board').prop(
'disabled',
@ -970,7 +969,7 @@ BlazeComponent.extendComponent({
// Get the ID from the anchor element, not the span
const anchorElement = $(evt.target).closest('.js-field-show-parent-in-minicard')[0];
const value = anchorElement ? anchorElement.id : null;
if (value) {
Boards.update(this.currentBoard._id, { $set: { presentParentTask: value } });
}
@ -986,171 +985,6 @@ BlazeComponent.extendComponent({
this.currentBoard = Utils.getCurrentBoard();
},
allowsReceivedDate() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsReceivedDate : false;
},
allowsStartDate() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsStartDate : false;
},
allowsDueDate() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDueDate : false;
},
allowsEndDate() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsEndDate : false;
},
allowsSubtasks() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsSubtasks : false;
},
allowsCreator() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? (currentBoard.allowsCreator ?? false) : false;
},
allowsCreatorOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? (currentBoard.allowsCreatorOnMinicard ?? false) : false;
},
allowsMembers() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsMembers : false;
},
allowsAssignee() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignee : false;
},
allowsAssignedBy() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAssignedBy : false;
},
allowsRequestedBy() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsRequestedBy : false;
},
allowsCardSortingByNumber() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardSortingByNumber : false;
},
allowsShowLists() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsShowLists : false;
},
allowsLabels() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsLabels : false;
},
allowsShowListsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsShowListsOnMinicard : false;
},
allowsChecklists() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsChecklists : false;
},
allowsAttachments() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsAttachments : false;
},
allowsComments() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsComments : false;
},
allowsCardNumber() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardNumber : false;
},
allowsDescriptionTitle() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTitle : false;
},
allowsDescriptionText() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionText : false;
},
isBoardSelected() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.dateSettingsDefaultBoardID : false;
},
isNullBoardSelected() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? (
currentBoard.dateSettingsDefaultBoardId === null ||
currentBoard.dateSettingsDefaultBoardId === undefined
) : true;
},
allowsDescriptionTextOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsDescriptionTextOnMinicard : false;
},
allowsCoverAttachmentOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCoverAttachmentOnMinicard : false;
},
allowsBadgeAttachmentOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsBadgeAttachmentOnMinicard : false;
},
allowsCardSortingByNumberOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
return currentBoard ? currentBoard.allowsCardSortingByNumberOnMinicard : false;
},
boards() {
const ret = ReactiveCache.getBoards(
{
@ -1191,261 +1025,228 @@ BlazeComponent.extendComponent({
{
'click .js-field-has-receiveddate'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsReceivedDate;
Boards.update(this.currentBoard._id, { $set: { allowsReceivedDate: newValue } });
const newValue = !Utils.allowsReceivedDate();
this.currentBoard.setAllowsReceivedDate(newValue);
},
'click .js-field-has-startdate'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsStartDate;
Boards.update(this.currentBoard._id, { $set: { allowsStartDate: newValue } });
const newValue = !Utils.allowsStartDate();
this.currentBoard.setAllowsStartDate(newValue);
},
'click .js-field-has-enddate'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsEndDate;
Boards.update(this.currentBoard._id, { $set: { allowsEndDate: newValue } });
const newValue = !Utils.allowsEndDate();
this.currentBoard.setAllowsEndDate(newValue);
},
'click .js-field-has-duedate'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsDueDate;
Boards.update(this.currentBoard._id, { $set: { allowsDueDate: newValue } });
const newValue = !Utils.allowsDueDate();
this.currentBoard.setAllowsDueDate(newValue);
},
'click .js-field-has-subtasks'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsSubtasks;
Boards.update(this.currentBoard._id, { $set: { allowsSubtasks: newValue } });
const newValue = !Utils.allowsSubtasks();
this.currentBoard.setAllowsSubtasks(newValue);
},
'click .js-field-has-creator'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsCreator;
Boards.update(this.currentBoard._id, { $set: { allowsCreator: newValue } });
const newValue = !Utils.allowsCreator();
this.currentBoard.setAllowsCreator(newValue);
},
'click .js-field-has-creator-on-minicard'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsCreatorOnMinicard;
Boards.update(this.currentBoard._id, { $set: { allowsCreatorOnMinicard: newValue } });
const newValue = !Utils.allowsCreatorOnMinicard();
this.currentBoard.setAllowsCreatorOnMinicard(newValue);
},
'click .js-field-has-members'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsMembers;
Boards.update(this.currentBoard._id, { $set: { allowsMembers: newValue } });
const newValue = !Utils.allowsMembers();
this.currentBoard.setAllowsMembers(newValue);
},
'click .js-field-has-assignee'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsAssignee;
Boards.update(this.currentBoard._id, { $set: { allowsAssignee: newValue } });
const newValue = !Utils.allowsAssignee();
this.currentBoard.setAllowsAssignee(newValue);
},
'click .js-field-has-assigned-by'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsAssignedBy;
Boards.update(this.currentBoard._id, { $set: { allowsAssignedBy: newValue } });
const newValue = !Utils.allowsAssignedBy();
this.currentBoard.setAllowsAssignedBy(newValue);
},
'click .js-field-has-requested-by'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsRequestedBy;
Boards.update(this.currentBoard._id, { $set: { allowsRequestedBy: newValue } });
const newValue = !Utils.allowsRequestedBy();
this.currentBoard.setAllowsRequestedBy(newValue);
},
'click .js-field-has-card-sorting-by-number'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsCardSortingByNumber;
Boards.update(this.currentBoard._id, { $set: { allowsCardSortingByNumber: newValue } });
const newValue = !Utils.allowsCardSortingByNumber();
this.currentBoard.setAllowsCardSortingByNumber(newValue);
},
'click .js-field-has-card-show-lists'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsShowLists;
Boards.update(this.currentBoard._id, { $set: { allowsShowLists: newValue } });
const newValue = !Utils.allowsShowLists();
this.currentBoard.setAllowsShowLists(newValue);
},
'click .js-field-has-labels'(evt) {
evt.preventDefault();
const newValue = !this.currentBoard.allowsLabels;
Boards.update(this.currentBoard._id, { $set: { allowsLabels: newValue } });
const newValue = !Utils.allowsLabels();
this.currentBoard.setAllowsLabels(newValue);
},
'click .js-field-has-card-show-lists-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsShowListsOnMinicard = !this.currentBoard
.allowsShowListsOnMinicard;
this.currentBoard.setAllowsShowListsOnMinicard(
this.currentBoard.allowsShowListsOnMinicard,
);
const newValue = !Utils.allowsShowListsOnMinicard();
this.currentBoard.setAllowsShowListsOnMinicard(newValue);
$(`.js-field-has-card-show-lists-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsShowListsOnMinicard,
Utils.allowsShowListsOnMinicard(),
);
$('.js-field-has-card-show-lists-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsShowListsOnMinicard,
Utils.allowsShowListsOnMinicard(),
);
},
'click .js-field-has-description-title'(evt) {
evt.preventDefault();
this.currentBoard.allowsDescriptionTitle = !this.currentBoard
.allowsDescriptionTitle;
this.currentBoard.setAllowsDescriptionTitle(
this.currentBoard.allowsDescriptionTitle,
);
const newValue = !Utils.allowsDescriptionTitle();
this.currentBoard.setAllowsDescriptionTitle(newValue);
$(`.js-field-has-description-title ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionTitle,
Utils.allowsDescriptionTitle(),
);
$('.js-field-has-description-title').toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionTitle,
Utils.allowsDescriptionTitle(),
);
},
'click .js-field-has-card-number'(evt) {
evt.preventDefault();
this.currentBoard.allowsCardNumber = !this.currentBoard
.allowsCardNumber;
this.currentBoard.setAllowsCardNumber(
this.currentBoard.allowsCardNumber,
);
const newValue = !Utils.allowsCardNumber();
this.currentBoard.setAllowsCardNumber(newValue);
$(`.js-field-has-card-number ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsCardNumber,
Utils.allowsCardNumber(),
);
$('.js-field-has-card-number').toggleClass(
CKCLS,
this.currentBoard.allowsCardNumber,
Utils.allowsCardNumber(),
);
},
'click .js-field-has-description-text-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsDescriptionTextOnMinicard = !this.currentBoard
.allowsDescriptionTextOnMinicard;
this.currentBoard.setallowsDescriptionTextOnMinicard(
this.currentBoard.allowsDescriptionTextOnMinicard,
);
const newValue = !Utils.allowsDescriptionTextOnMinicard();
this.currentBoard.setAllowsDescriptionTextOnMinicard(newValue);
$(`.js-field-has-description-text-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionTextOnMinicard,
Utils.allowsDescriptionTextOnMinicard(),
);
$('.js-field-has-description-text-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionTextOnMinicard,
Utils.allowsDescriptionTextOnMinicard(),
);
},
'click .js-field-has-description-text'(evt) {
evt.preventDefault();
this.currentBoard.allowsDescriptionText = !this.currentBoard
.allowsDescriptionText;
this.currentBoard.setAllowsDescriptionText(
this.currentBoard.allowsDescriptionText,
);
const newValue = !Utils.allowsDescriptionText();
this.currentBoard.setAllowsDescriptionText(newValue);
$(`.js-field-has-description-text ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionText,
Utils.allowsDescriptionText(),
);
$('.js-field-has-description-text').toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionText,
Utils.allowsDescriptionText(),
);
},
'click .js-field-has-checklists'(evt) {
evt.preventDefault();
this.currentBoard.allowsChecklists = !this.currentBoard
.allowsChecklists;
this.currentBoard.setAllowsChecklists(
this.currentBoard.allowsChecklists,
);
const newValue = !Utils.allowsChecklists();
this.currentBoard.setAllowsChecklists(newValue);
$(`.js-field-has-checklists ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsChecklists,
Utils.allowsChecklists(),
);
$('.js-field-has-checklists').toggleClass(
CKCLS,
this.currentBoard.allowsChecklists,
Utils.allowsChecklists(),
);
},
'click .js-field-has-attachments'(evt) {
evt.preventDefault();
this.currentBoard.allowsAttachments = !this.currentBoard
.allowsAttachments;
this.currentBoard.setAllowsAttachments(
this.currentBoard.allowsAttachments,
);
const newValue = !Utils.allowsAttachments();
this.currentBoard.setAllowsAttachments(newValue);
$(`.js-field-has-attachments ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAttachments,
Utils.allowsAttachments(),
);
$('.js-field-has-attachments').toggleClass(
CKCLS,
this.currentBoard.allowsAttachments,
Utils.allowsAttachments(),
);
},
'click .js-field-has-comments'(evt) {
evt.preventDefault();
this.currentBoard.allowsComments = !this.currentBoard.allowsComments;
this.currentBoard.setAllowsComments(this.currentBoard.allowsComments);
const newValue = !Utils.allowsComments();
this.currentBoard.setAllowsComments(newValue);
$(`.js-field-has-comments ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsComments,
Utils.allowsComments(),
);
$('.js-field-has-comments').toggleClass(
CKCLS,
this.currentBoard.allowsComments,
Utils.allowsComments(),
);
},
'click .js-field-has-activities'(evt) {
evt.preventDefault();
this.currentBoard.allowsActivities = !this.currentBoard
.allowsActivities;
this.currentBoard.setAllowsActivities(
this.currentBoard.allowsActivities,
);
const newValue = !Utils.allowsActivities();
this.currentBoard.setAllowsActivities(newValue);
$(`.js-field-has-activities ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsActivities,
Utils.allowsActivities(),
);
$('.js-field-has-activities').toggleClass(
CKCLS,
this.currentBoard.allowsActivities,
Utils.allowsActivities(),
);
},
'click .js-field-has-cover-attachment-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsCoverAttachmentOnMinicard = !this.currentBoard
.allowsCoverAttachmentOnMinicard;
this.currentBoard.setallowsCoverAttachmentOnMinicard(
this.currentBoard.allowsCoverAttachmentOnMinicard,
);
const newValue = !Utils.allowsCoverAttachmentOnMinicard();
this.currentBoard.setAllowsCoverAttachmentOnMinicard(newValue);
$(`.js-field-has-cover-attachment-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsCoverAttachmentOnMinicard,
Utils.allowsCoverAttachmentOnMinicard(),
);
$('.js-field-has-cover-attachment-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsCoverAttachmentOnMinicard,
Utils.allowsCoverAttachmentOnMinicard(),
);
},
'click .js-field-has-badge-attachment-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsBadgeAttachmentOnMinicard = !this.currentBoard
.allowsBadgeAttachmentOnMinicard;
this.currentBoard.setallowsBadgeAttachmentOnMinicard(
this.currentBoard.allowsBadgeAttachmentOnMinicard,
);
const newValue = !Utils.allowsBadgeAttachmentOnMinicard();
this.currentBoard.setAllowsBadgeAttachmentOnMinicard(newValue);
$(`.js-field-has-badge-attachment-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsBadgeAttachmentOnMinicard,
Utils.allowsBadgeAttachmentOnMinicard(),
);
$('.js-field-has-badge-attachment-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsBadgeAttachmentOnMinicard,
Utils.allowsBadgeAttachmentOnMinicard(),
);
},
'click .js-field-has-card-sorting-by-number-on-minicard'(evt) {
evt.preventDefault();
this.currentBoard.allowsCardSortingByNumberOnMinicard = !this.currentBoard
.allowsCardSortingByNumberOnMinicard;
this.currentBoard.setallowsCardSortingByNumberOnMinicard(
this.currentBoard.allowsCardSortingByNumberOnMinicard,
);
const newValue = !Utils.allowsCardSortingByNumberOnMinicard();
this.currentBoard.setAllowsCardSortingByNumberOnMinicard(newValue);
$(`.js-field-has-card-sorting-by-number-on-minicard ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsCardSortingByNumberOnMinicard,
Utils.allowsCardSortingByNumberOnMinicard(),
);
$('.js-field-has-card-sorting-by-number-on-minicard').toggleClass(
CKCLS,
this.currentBoard.allowsCardSortingByNumberOnMinicard,
Utils.allowsCardSortingByNumberOnMinicard(),
);
},
},
@ -1541,12 +1342,13 @@ BlazeComponent.extendComponent({
'keyup .js-search-member-input'(event) {
Session.set('addMemberPopup.error', '');
const query = event.target.value.trim();
this.searchQuery.set(query);
// Clear previous timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// Debounce search
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
@ -2061,4 +1863,3 @@ Template.changePermissionsPopup.helpers({
);
},
});

View file

@ -1,3 +0,0 @@
input {
max-width: 100%;
}

View file

@ -14,11 +14,8 @@ BlazeComponent.extendComponent({
},
clickOnMiniCard(evt) {
if (Utils.isMiniScreen()) {
evt.preventDefault();
Session.set('popupCardId', this.currentData()._id);
this.cardDetailsPopup(evt);
}
evt.preventDefault();
Session.set('popupCardId', this.currentData()._id);
},
cardDetailsPopup(event) {

View file

@ -9,41 +9,37 @@ template(name="swimlaneHeader")
+swimlaneFixedHeader(this)
template(name="swimlaneFixedHeader")
.swimlane-header(
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
if $eq title 'Card Templates'
| {{_ 'card-templates-swimlane'}}
else if $eq title 'List Templates'
| {{_ 'list-templates-swimlane'}}
else if $eq title 'Board Templates'
| {{_ 'board-templates-swimlane'}}
else if $eq title 'Default'
| {{_ 'defaultdefault'}}
else
+viewer
| {{isTitleDefault title}}
.swimlane-header-menu
.swimlane-header-menu-left
if currentUser
unless currentUser.isCommentOnly
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
unless currentUser.isWorker
a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
if collapseSwimlane
i.fa.fa-caret-right
else
i.fa.fa-caret-down
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
i.fa.fa-bars
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
i.fa.fa-plus
if isTouchScreenOrShowDesktopDragHandles
unless isTouchScreen
a.swimlane-header-handle.handle.js-swimlane-header-handle
i.fa.fa-arrows
if isTouchScreen
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
i.fa.fa-arrows
unless currentUser.isWorker
a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
if collapseSwimlane
i.fa.fa-caret-right
else
i.fa.fa-caret-down
.swimlane-header(
class="{{#if currentUser.isBoardMember}}js-open-inlined-form is-editable{{/if}}")
if $eq title 'Card Templates'
| {{_ 'card-templates-swimlane'}}
else if $eq title 'List Templates'
| {{_ 'list-templates-swimlane'}}
else if $eq title 'Board Templates'
| {{_ 'board-templates-swimlane'}}
else if $eq title 'Default'
| {{_ 'defaultdefault'}}
else
+viewer
| {{isTitleDefault title}}
.swimlane-header-menu-right
if currentUser
unless currentUser.isCommentOnly
unless currentUser.isWorker
a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
i.fa.fa-bars
if isMiniScreen
a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
i.fa.fa-arrows
template(name="editSwimlaneTitleForm")
.list-composer
@ -59,23 +55,23 @@ template(name="swimlaneActionPopup")
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
ul.pop-over-list
li: a.js-add-swimlane
i.fa.fa-plus
span {{_ 'add-swimlane'}}
li: a.js-add-swimlane
i.fa.fa-plus
span {{_ 'add-swimlane'}}
hr
ul.pop-over-list
li: a.js-add-list-from-swimlane
i.fa.fa-plus
span {{_ 'add-list'}}
li: a.js-add-list-from-swimlane
i.fa.fa-plus
span {{_ 'add-list'}}
hr
ul.pop-over-list
if currentUser.isBoardAdmin
li: a.js-set-swimlane-color
i.fa.fa-paint-brush
| {{_ 'select-color'}}
li: a.js-set-swimlane-height
i.fa.fa-arrows
| {{_ 'set-swimlane-height'}}
if currentUser.isBoardAdmin
li: a.js-set-swimlane-color
i.fa.fa-paint-brush
| {{_ 'select-color'}}
li: a.js-set-swimlane-height
i.fa.fa-arrows
| {{_ 'set-swimlane-height'}}
if currentUser.isBoardAdmin
unless this.isTemplateContainer
hr
@ -117,8 +113,7 @@ template(name="setSwimlaneColorPopup")
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
.form-buttons
button.primary.confirm.js-submit(style="margin-left:0") {{_ 'save'}}
button.js-remove-color.negate.wide.right(style="margin-left:8px") {{_ 'unset-color'}}

View file

@ -86,7 +86,7 @@ Template.editSwimlaneTitleForm.helpers({
// When that happens, try use translation "defaultdefault" that has same content of default, or return text "Default".
// This can happen, if swimlane does not have name.
// Yes, this is fixing the symptom (Swimlane title does not have title)
// instead of fixing the problem (Add Swimlane title when creating swimlane)
// instead of fixing the problem (Add Swimlane title when creating swimlane)
// because there could be thousands of swimlanes, adding name Default to all of them
// would be very slow.
if (title.startsWith("key 'default") && title.endsWith('returned an object instead of string.')) {

View file

@ -1,39 +1,29 @@
[class=swimlane] {
position: sticky;
left: 0;
}
.swimlane {
.swimlane.js-lists{
background: #dedede;
display: flex;
flex-direction: row;
overflow: auto;
max-height: 100%;
position: relative;
flex-direction: row;
box-sizing: border-box;
height: var(--swimlane-height, auto);
min-height: var(--swimlane-min-height, 200px);
}
.swimlane.js-lists.js-swimlane {
min-height: 150px;
body.mobile-mode .swimlane {
display: flex;
flex-direction: column;
width: 100%;
.swimlane-header {
font-size: var(--header-scale);
}
}
.swimlane-header-menu .swimlane-header-collapse-down {
font-size: 50%;
color: #a6a6a6;
position: absolute;
top: 0.7vh;
left: 13vw;
}
.swimlane-header-menu .swimlane-header-collapse-up {
font-size: 50%;
color: #a6a6a6;
position: absolute;
bottom: 0.7vh;
left: 13vw;
}
.swimlane-header-menu .swimlane-header-uncollapse-up {
font-size: 50%;
color: #a6a6a6;
}
.swimlane-header-menu .swimlane-header-uncollapse-down {
font-size: 50%;
color: #a6a6a6;
.swimlane-container {
background-color: #ccc;
display: flex;
flex: 1;
flex-direction: column;
/* default to the same as lists to avoid contrast with the handle */
background: #dedede;
}
.swimlane.placeholder {
background-color: rgba(0,0,0,0.2);
@ -50,30 +40,28 @@
cursor: grabbing;
}
.swimlane .swimlane-header-wrap {
overflow: hidden;
display: flex;
flex-direction: row;
flex: 1 0 100%;
flex: 1;
align-items: center;
justify-content: space-between;
height: max-content;
padding: 0.5lh 1ch;
background-color: #ccc;
width: 100%;
min-width: 100%;
position: relative;
overflow: visible;
min-height: 33px;
padding: 0;
margin: 0;
position: sticky;
left: 0;
p {
margin: 0;
}
}
.swimlane .swimlane-header-wrap .swimlane-header {
font-size: 14px;
padding: 0;
font-weight: bold;
min-height: 33px;
width: 100%;
overflow: hidden;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
word-wrap: break-word;
text-align: center;
position: relative;
overflow-wrap: break-word;
z-index: 10;
pointer-events: auto;
display: flex;
@ -81,87 +69,30 @@
justify-content: center;
line-height: 1.2;
}
.swimlane .swimlane-header-wrap .swimlane-header-menu {
position: absolute;
top: 0;
left: 0;
padding: 0;
margin: 0;
font-size: 22px;
line-height: 1;
z-index: 20;
pointer-events: auto;
}
.swimlane .swimlane-header-wrap .swimlane-header-menu .js-open-swimlane-menu {
top: calc(50% + 6px);
padding: 5px;
display: inline-block;
margin-left: 30px;
color: #a6a6a6;
vertical-align: middle;
line-height: 1.2;
.swimlane {
.swimlane-header-menu-right, .swimlane-header-menu-left {
display: inline-flex;
align-content: center;
gap: 2ch;
}
/* can't resize beyond that point, but resizing screen causes
overflow, which is great because lists would shrink too much otherwise */
max-width: 100vw;
}
@media print {
.swimlane .swimlane-header-wrap .swimlane-header-menu {
.swimlane .swimlane-header-wrap .swimlane-header-menu-right {
display: none;
}
}
.swimlane .swimlane-header-wrap .swimlane-header-plus-icon {
top: calc(50% + 6px);
padding: 5px;
margin-left: 20px;
font-size: 22px;
color: #a6a6a6;
}
.swimlane .swimlane-header-wrap .swimlane-header-menu-icon {
top: calc(50% + 6px);
padding-left: 5px;
font-size: 22px;
}
.swimlane .swimlane-header-wrap .swimlane-header-handle {
position: relative;
top: calc(50% + 2px);
padding: 2px 5px;
font-size: clamp(16px, 3vw, 20px);
display: inline-block;
vertical-align: middle;
margin-left: 30px;
cursor: move;
pointer-events: auto;
color: #a6a6a6;
line-height: 1.2;
}
.swimlane .swimlane-header-wrap .swimlane-header-miniscreen-handle {
position: relative;
padding: 2px 5px;
top: calc(50% + 2px);
font-size: 24px;
display: inline-block;
vertical-align: middle;
margin-left: 30px;
cursor: move;
pointer-events: auto;
color: #a6a6a6;
}
/* Swimlane collapse button styling - matches list collapse button */
.swimlane .swimlane-header-wrap .swimlane-header-menu .swimlane-collapse-indicator {
.swimlane .swimlane-header-wrap .swimlane-header-handle {
cursor: move;
pointer-events: auto;
color: #a6a6a6;
display: inline-block;
vertical-align: middle;
padding: 5px;
border: none;
border-radius: 0;
background-color: transparent;
cursor: pointer;
font-size: 18px;
line-height: 1.2;
text-align: center;
text-decoration: none;
margin: 0;
flex-shrink: 0;
}
.swimlane .swimlane-header-wrap .swimlane-header-menu .swimlane-collapse-indicator:hover {
.swimlane .swimlane-header-wrap .swimlane-header-menu-right .swimlane-collapse-indicator:hover {
background-color: transparent;
color: #333;
}
@ -290,105 +221,75 @@
color: #fff !important;
}
body.mobile-mode {
.swimlane-resize-handle {
height: 2ch;
:active {
background: rgba(0, 123, 255, 0.4) !important;
}
}
}
body.mobile-mode {
.swimlane-resize-handle {
height: 1lh;
}
}
/* Swimlane resize handle */
.swimlane-resize-handle {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 8px;
background: transparent;
height: max(0.7ch, 0.3lh);
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 {
/* Prevent scrolling behaviour on click */
touch-action: none;
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);
box-sizing: border-box;
}
/* Add a subtle indicator line */
.swimlane-resize-handle::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
left: 50vw;
width: 20px;
height: 2px;
background: rgba(0, 123, 255, 0.6);
border-radius: 1px;
height: 1px;
background: rgba(0, 0, 0, 0.2);
border-radius: 5px;
opacity: 0;
transition: opacity 0.2s ease;
}
.swimlane-resize-handle:hover::before {
opacity: 1;
.swimlane.swimlane-resizing + .swimlane-resize-handle:hover::before, .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;
.swimlane:not(.cannot-resize) {
/* Add a subtle resize indicator line at the bottom of swimlane on hover */
&:hover + .swimlane-resize-handle, + .swimlane-resize-handle:hover {
border-top: 1px solid rgba(0, 123, 255, 0.5);
background: rgba(0, 123, 255, 0.2);
border-radius: 0;
}
}
.swimlane.swimlane-resizing + .swimlane-resize-handle {
background: rgba(0, 123, 255, 0.4) !important;
}
.swimlane.cannot-resize + .swimlane-resize-handle {
background: rgba(227, 64, 83, 0.5) !important;
border-radius: 0;
}
body.swimlane-resizing-active {
cursor: row-resize !important;
user-select: none !important;
}
body.swimlane-resizing-active * {
cursor: row-resize !important;
user-select: none !important;
}

View file

@ -1,43 +1,38 @@
template(name="swimlane")
.swimlane.nodragscroll
+swimlaneHeader
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)
unless currentList
.swimlane-container
.swimlane.nodragscroll
+swimlaneHeader
unless collapseSwimlane
.swimlane.js-lists.js-swimlane.dragscroll(id="swimlane-{{_id}}")
if isMiniScreen
each lists
+miniList(this)
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
else
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
each lists
+miniList(this)
else
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
each lists
if visible this
+list(this)
if currentCardIsInThisList _id ../_id
+cardDetails(currentCard)
if visible this
+list(this)
//- allow resizing in mobile mode
.swimlane-resize-handle.js-swimlane-resize-handle.nodragscroll
template(name="listsGroup")
.swimlane.list-group.js-lists.dragscroll
if isMiniScreen
if currentList
+list(currentList)
else
each lists
+miniList(this)
each lists
+miniList(this)
if currentUser.isBoardMember
unless currentUser.isCommentOnly
+addListForm
else
each lists
if visible this
+list(this)
if currentCardIsInThisList _id null
+cardDetails(currentCard)
.swimlane-resize-handle.js-swimlane-resize-handle.nodragscroll
template(name="addListForm")
unless currentUser.isWorker
@ -45,27 +40,27 @@ template(name="addListForm")
unless currentUser.isReadOnly
unless currentUser.isReadAssignedOnly
.list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
+inlinedForm(autoclose=false)
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
autocomplete="off" autofocus)
if currentBoard.getLastList
| {{_ 'add-after-list'}}
select.list-position-input.full-line
each currentBoard.lists
option(value="{{_id}}" selected=currentBoard.getLastList.title) {{title}}
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
.js-close-inlined-form
i.fa.fa-times-thin
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-list-template {{_ 'template'}}
else
a.open-list-composer.js-open-inlined-form(title="{{_ 'add-list'}}")
i.fa.fa-plus
.list-header-add
+inlinedForm(autoclose=false)
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
autocomplete="off" autofocus)
if lists
| {{_ 'add-after-list'}}
select.list-position-input.full-line
each lists
option(value="{{_id}}" selected=currentBoard.getLastList.title) {{title}}
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
a.js-close-inlined-form
i.fa.fa-times-thin
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-list-template {{_ 'template'}}
else
a.open-list-composer.list-header.js-open-inlined-form(title="{{_ 'add-list'}}")
i.fa.fa-plus
template(name="moveSwimlanePopup")
if currentUser

View file

@ -2,6 +2,12 @@ import { ReactiveCache } from '/imports/reactiveCache';
import dragscroll from '@wekanteam/dragscroll';
const { calculateIndex } = Utils;
function getBoardComponent() {
// as list can be rendered from multiple inner elements, feels like a reliable
// way to get the components having rendered the board
return BlazeComponent.getComponentForElement(document.getElementsByClassName('board-canvas')[0]);
}
function saveSorting(ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following list -- if any.
@ -152,6 +158,12 @@ function currentListIsInThisSwimlane(swimlaneId) {
);
}
function currentList(listId, swimlaneId) {
const list = Utils.getCurrentList();
return list && list._id == listId && (list.swimlaneId === swimlaneId || list.swimlaneId === '');
}
function currentCardIsInThisList(listId, swimlaneId) {
const currentCard = Utils.getCurrentCard();
//const currentUser = ReactiveCache.getCurrentUser();
@ -227,122 +239,63 @@ function syncListOrderFromStorage(boardId) {
}
};
function initSortable(boardComponent, $listsDom) {
// Safety check: ensure we have valid DOM elements
if (!$listsDom || $listsDom.length === 0) {
console.error('initSortable: No valid DOM elements provided');
return;
}
// Check if sortable is already initialized
if ($listsDom.data('uiSortable') || $listsDom.data('sortable')) {
$listsDom.sortable('destroy');
}
// We want to animate the card details window closing. We rely on CSS
// transition for the actual animation.
$listsDom._uihooks = {
removeElement(node) {
const removeNode = _.once(() => {
node.parentNode.removeChild(node);
});
if ($(node).hasClass('js-card-details')) {
$(node).css({
flexBasis: 0,
padding: 0,
});
$listsDom.one(CSSEvents.transitionend, removeNode);
} else {
removeNode();
}
},
};
// Add click debugging for drag handles
$listsDom.on('mousedown', '.js-list-handle', function(e) {
e.stopPropagation();
});
$listsDom.on('mousedown', '.js-list-header', function(e) {
});
// Add debugging for any mousedown on lists
$listsDom.on('mousedown', '.js-list', function(e) {
});
// Add debugging for sortable events
$listsDom.on('sortstart', function(e, ui) {
});
$listsDom.on('sortbeforestop', function(e, ui) {
});
$listsDom.on('sortstop', function(e, ui) {
});
try {
$listsDom.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper(evt, item) {
const helper = item.clone();
helper.css('z-index', 1000);
return helper;
},
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 3,
forcePlaceholderSize: true,
cursor: 'move',
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
// Add visual feedback for list being dragged
ui.item.addClass('ui-sortable-helper');
// Disable dragscroll during list dragging to prevent interference
try {
dragscroll.reset();
} catch (e) {
}
// Also disable dragscroll on all swimlanes during list dragging
$('.js-swimlane').each(function() {
$(this).removeClass('dragscroll');
});
},
beforeStop(evt, ui) {
// Clean up visual feedback
ui.item.removeClass('ui-sortable-helper');
},
stop(evt, ui) {
saveSorting(ui);
}
});
} catch (error) {
console.error('Error initializing list sortable:', error);
return;
}
// Check if drag handles exist
const dragHandles = $listsDom.find('.js-list-handle');
// Check if lists exist
const lists = $listsDom.find('.js-list');
// Skip the complex autorun and options for now
}
BlazeComponent.extendComponent({
initializeSortableLists() {
let boardComponent = getBoardComponent();
// needs to be run again on uncollapsed
const handleSelector = Utils.isMiniScreen()
? '.js-list-handle'
: '.list-header-name-container';
const $lists = this.$('.js-list');
const $parent = $lists.parent();
if ($lists.length > 0) {
// Check for drag handles
const $handles = $parent.find(handleSelector);
// Test if drag handles are clickable
$handles.on('click', function (e) {
e.preventDefault();
e.stopPropagation();
});
$parent.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper: 'clone',
items: '.js-list',
placeholder: 'list placeholder',
distance: 7,
handle: handleSelector,
disabled: !Utils.canModifyBoard(),
start(evt, ui) {
ui.helper.css('z-index', 1000);
width = ui.helper.width();
height = ui.helper.height();
ui.placeholder.height(height);
ui.placeholder.width(width);
ui.placeholder[0].setAttribute('style', `width: ${width}px !important; height: ${height}px !important;`);
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
saveSorting(ui);
},
sort(event, ui) {
Utils.scrollIfNeeded(event);
},
});
}
},
onRendered() {
const boardComponent = this.parentComponent();
// can be rendered from either swimlane or board; check with DOM class heuristic,
const $listsDom = this.$('.js-lists');
// Sync list order from localStorage on board load
const boardId = Session.get('currentBoard');
@ -353,68 +306,18 @@ BlazeComponent.extendComponent({
}, 500);
}
if (!Utils.getCurrentCardId()) {
boardComponent.scrollLeft();
}
// Try a simpler approach - initialize sortable directly like cards do
this.initializeSwimlaneResize();
// Wait for DOM to be ready
setTimeout(() => {
const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles()
? '.js-list-handle'
: '.js-list-header';
const $lists = this.$('.js-list');
setTimeout(this.initializeSortableLists, 100);
const $parent = $lists.parent();
if ($lists.length > 0) {
// Check for drag handles
const $handles = $parent.find('.js-list-handle');
// Test if drag handles are clickable
$handles.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
});
$parent.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
placeholder: 'list placeholder',
distance: 7,
handle: handleSelector,
disabled: !Utils.canModifyBoard(),
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
saveSorting(ui);
}
});
// Reactively update handle when user toggles desktop drag handles
this.autorun(() => {
const newHandle = Utils.isTouchScreenOrShowDesktopDragHandles()
? '.js-list-handle'
: '.js-list-header';
if ($parent.data('uiSortable') || $parent.data('sortable')) {
try { $parent.sortable('option', 'handle', newHandle); } catch (e) {}
}
});
} else {
// React to uncollapse (data is always reactive)
this.autorun(() => {
if (!this.currentData().isCollapsed()) {
this.initializeSortableLists();
}
}, 100);
});
},
onCreated() {
this.draggingActive = new ReactiveVar(false);
@ -465,7 +368,7 @@ BlazeComponent.extendComponent({
// his mouse.
const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
Utils.isTouchScreenOrShowDesktopDragHandles()
Utils.isMiniScreen()
? ['.js-list-handle', '.js-swimlane-header-handle']
: ['.js-list-header'],
).concat([
@ -477,7 +380,7 @@ BlazeComponent.extendComponent({
const isInNoDragArea = $(evt.target).closest(noDragInside.join(',')).length > 0;
if (isResizeHandle) {
return;
//return;
}
if (
@ -512,6 +415,11 @@ BlazeComponent.extendComponent({
},
swimlaneHeight() {
// Using previous size with so much collasped/vertical logic will probably
// be worst that letting layout takes needed space given the opened list each time
if (Utils.isMiniScreen()) {
return;
}
const user = ReactiveCache.getCurrentUser();
const swimlane = Template.currentData();
@ -552,7 +460,7 @@ BlazeComponent.extendComponent({
const swimlane = Template.currentData();
const $swimlane = $(`#swimlane-${swimlane._id}`);
const $resizeHandle = $swimlane.find('.js-swimlane-resize-handle');
const $resizeHandle = $swimlane.siblings('.js-swimlane-resize-handle');
// Check if elements exist
if (!$swimlane.length || !$resizeHandle.length) {
@ -570,76 +478,190 @@ BlazeComponent.extendComponent({
return;
}
const isTouchScreen = Utils.isTouchScreen();
let isResizing = false;
let startY = 0;
let startHeight = 0;
const minHeight = 100;
const maxHeight = 2000;
const minHeight = Utils.isMiniScreen() ? 200 : 50;
const absoluteMaxHeight = 2000;
let computingHeight;
let frame;
let fullHeight, maxHeight;
let pageY, screenY, deltaY;
// how to do cleaner?
const flexContainer = document.getElementsByClassName('swim-flex')[0];
// only for cosmetic
let maxHeightWithTolerance;
const tolerance = 30;
let previousLimit = false;
$swimlane[0].style.setProperty('--swimlane-min-height', `${minHeight}px`);
// avoid jump effect and ensure height stays consistent
// ⚠️ here, I propose to ignore saved height if it is not filled by content.
// having large portions of blank lists makes the layout strange and hard to
// navigate; also, the height changes a lot between different views, so it
// feels ok to use the size as a hint, not as an absolute (as a user also)
const unconstraignedHeight = $swimlane[0].getBoundingClientRect().height;
const userHeight = parseFloat(this.swimlaneHeight(), 10);
const preferredHeight = Math.min(userHeight, absoluteMaxHeight, unconstraignedHeight);
$swimlane[0].style.setProperty('--swimlane-height', `${preferredHeight}px`);
const startResize = (e) => {
isResizing = true;
startY = e.pageY || e.originalEvent.touches[0].pageY;
startHeight = parseInt($swimlane.css('height')) || 300;
// gain access to modern attributes e.g. isPrimary
e = e.originalEvent;
$swimlane.addClass('swimlane-resizing');
$('body').addClass('swimlane-resizing-active');
$('body').css('user-select', 'none');
e.preventDefault();
e.stopPropagation();
};
const doResize = (e) => {
if (!isResizing) {
if (isResizing || !(e.isPrimary && (e.pointerType !== 'mouse' || e.button === 0))) {
return;
}
const currentY = e.pageY || e.originalEvent.touches[0].pageY;
const deltaY = currentY - startY;
const newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY));
waitHeight(e, startResizeKnowingHeight);
};
// unsure about this one; this is a way to compute what would be a "fit-content" height,
// so that user cannot drag the swimlane too far. to do so, we clone the swimlane add
// add it to the body, taking care of catching the frame just before it would be rendered.
// it is well supported by browsers and adds extra-computation only once, when start dragging,
// but still it feels odd.
// the reason we cannot use initial, computed height is because it could have changed because
// on new cards, thus constraining dragging too much. it is simple for list, add "real" unconstrained
// width do not update on adding cards.
const waitHeight = (e, callback) => {
const computeSwimlaneHeight = (_) => {
if (!computingHeight) {
computingHeight = $swimlane[0].cloneNode(true);
computingHeight.id = "clonedSwimlane";
$(computingHeight).attr('style', 'height: auto !important; position: absolute');
frame = requestAnimationFrame(computeSwimlaneHeight);
document.body.appendChild(computingHeight);
return;
}
catchBeforeRender = document.getElementById('clonedSwimlane');
if (catchBeforeRender) {
fullHeight = catchBeforeRender.offsetHeight;
if (fullHeight > 0) {
cancelAnimationFrame(frame);
document.body.removeChild(computingHeight);
computingHeight = undefined;
frame = undefined;
callback(e, fullHeight);
return;
}
}
frame = requestAnimationFrame(computeSwimlaneHeight);
}
computeSwimlaneHeight();
}
// 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');
const startResizeKnowingHeight = (e, height) => {
document.addEventListener('pointermove', doResize);
// e.g. debugger can cancel event without pointerup being fired
// document.addEventListener('pointercancel', stopResize);
document.addEventListener('pointerup', stopResize);
// unavailable on e.g. Safari but mostly for smoothness
document.addEventListener('wheel', doResize);
// --swimlane-height can be either a stored size or "auto"; get actual computed size
currentHeight = $swimlane[0].offsetHeight;
$swimlane.addClass('swimlane-resizing');
$('body').addClass('swimlane-resizing-active');
e.preventDefault();
e.stopPropagation();
// not being able to resize can be frustrating, give a little more room
maxHeight = Math.max(height, absoluteMaxHeight);
maxHeightWithTolerance = maxHeight + tolerance;
$swimlane[0].style.setProperty('--swimlane-max-height', `${maxHeightWithTolerance}px`);
pageY = e.pageY;
isResizing = true;
previousLimit = false;
deltaY = null;
}
const doResize = (e) => {
if (!isResizing || !(e.isPrimary || e instanceof WheelEvent)) {
return;
}
const { y: handleY, height: handleHeight } = $resizeHandle[0].getBoundingClientRect();
const containerHeight = flexContainer.offsetHeight;
const isBlocked = $swimlane[0].classList.contains('cannot-resize');
// deltaY of WheelEvent is unreliable, do with a simple actual delta with handle and pointer
deltaY = e.clientY - handleY;
const candidateHeight = currentHeight + deltaY;
const oldHeight = currentHeight;
let stepHeight = Math.max(minHeight, Math.min(maxHeightWithTolerance, candidateHeight));
const reachingMax = (maxHeightWithTolerance - stepHeight - 20) <= 0;
const reachingMin = (stepHeight - 20 - minHeight) <= 0;
if (!previousLimit && (reachingMax && deltaY > 0 || reachingMin && deltaY < 0)) {
$swimlane[0].classList.add('cannot-resize');
previousLimit = true;
if (reachingMax) {
stepHeight = maxHeightWithTolerance;
} else {
stepHeight = minHeight;
}
} else if (previousLimit && !reachingMax && !reachingMin) {
// we want to re-init only below handle if min-size, above if max-size,
// so computed values are accurate
if ((deltaY > 0 && pageY >= handleY + handleHeight)
|| (deltaY < 0 && pageY <= handleY)) {
$swimlane[0].classList.remove('cannot-resize');
// considered as a new move, changing direction is certain
previousLimit = false;
}
}
if (!isBlocked) {
// Ensure container grows and shrinks with swimlanes, so you guess a sense of scrolling something
if (e.pageY > (containerHeight - window.innerHeight)) {
document.body.style.height = `${containerHeight + window.innerHeight / 4}px`;
}
// helps to scroll at the beginning/end of the page
let gapToLeave = window.innerHeight / 10;
const factor = isTouchScreen ? 6 : 7;
if (e.clientY > factor * gapToLeave) {
//correct but too laggy
window.scrollBy({ top: gapToLeave, behavior: "smooth" });
}
// special case where scrolling down while
// swimlane is stuck; feels weird
else if (e.clientY < (10 - factor) * gapToLeave) {
window.scrollBy({ top: -gapToLeave , behavior: "smooth"});
}
}
if (oldHeight !== stepHeight && !isBlocked) {
// Apply the new height immediately for real-time feedback
$swimlane[0].style.setProperty('--swimlane-height', `${stepHeight}px`);
currentHeight = stepHeight;
}
};
const stopResize = (e) => {
if (!isResizing) return;
if(!isResizing) {
return;
}
if (previousLimit) {
$swimlane[0].classList.remove('cannot-resize');
}
// hopefully be gentler on cpu
document.removeEventListener('pointermove', doResize);
document.removeEventListener('pointercancel', stopResize);
document.removeEventListener('pointerup', stopResize);
document.removeEventListener('wheel', doResize);
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
let finalHeight = Math.min(parseInt($swimlane[0].style.getPropertyValue('--swimlane-height'), 10), maxHeight);
$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;
@ -678,30 +700,15 @@ BlazeComponent.extendComponent({
console.warn('Error saving swimlane height to localStorage:', e);
}
}
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();
});
// handle both pointer and touch
$resizeHandle.on("pointerdown", startResize);
},
}).register('swimlane');
BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Utils.getCurrentBoard();
@ -709,6 +716,8 @@ BlazeComponent.extendComponent({
this.currentBoard.isTemplatesBoard() &&
this.currentData().isListTemplatesSwimlane();
this.currentSwimlane = this.currentData();
// so that lists can be filtered from Board methods
this.currentBoard.swimlane = this.currentSwimlane;
},
// Proxy
@ -765,6 +774,13 @@ BlazeComponent.extendComponent({
},
}).register('addListForm');
Template.addListForm.helpers({
lists() {
return this.myLists();
}
});
Template.swimlane.helpers({
canSeeAddList() {
return ReactiveCache.getCurrentUser().isBoardAdmin();
@ -777,16 +793,14 @@ Template.swimlane.helpers({
collapseSwimlane() {
return Utils.getSwimlaneCollapseState(this);
}
},
});
// Initialize sortable on DOM elements
setTimeout(() => {
const $listsGroupElements = $('.list-group');
const computeHandle = () => (
Utils.isTouchScreenOrShowDesktopDragHandles() ? '.js-list-handle' : '.js-list-header'
);
const computeHandle = () => Utils.isMiniScreen() ? '.js-list-handle' : '.list-header-name-container';
// Initialize sortable on ALL listsGroup elements (even empty ones)
$listsGroupElements.each(function(index) {
@ -800,7 +814,7 @@ setTimeout(() => {
tolerance: 'pointer',
appendTo: '.board-canvas',
helper: 'clone',
items: '.js-list:not(.js-list-composer)',
items: '.js-list',
placeholder: 'list placeholder',
distance: 7,
handle: computeHandle(),
@ -820,29 +834,10 @@ setTimeout(() => {
// Silent fail
}
},
sort(event, ui) {
Utils.scrollIfNeeded(event);
},
stop(evt, ui) {
// To attribute the new index number, we need to get the DOM element
// of the previous and the following list -- if any.
const prevListDom = ui.item.prev('.js-list').get(0);
const nextListDom = ui.item.next('.js-list').get(0);
const sortIndex = calculateIndex(prevListDom, nextListDom, 1);
const listDomElement = ui.item.get(0);
if (!listDomElement) {
return;
}
let list;
try {
list = Blaze.getData(listDomElement);
} catch (error) {
return;
}
if (!list) {
return;
}
// Detect if the list was dropped in a different swimlane
const targetSwimlaneDom = ui.item.closest('.js-swimlane');
let targetSwimlaneId = null;
@ -949,18 +944,6 @@ setTimeout(() => {
} catch (e) {
// Silent fail
}
// Re-enable dragscroll after list dragging is complete
try {
dragscroll.reset();
} catch (e) {
// Silent fail
}
// Re-enable dragscroll on all swimlanes
$('.js-swimlane').each(function() {
$(this).addClass('dragscroll');
});
}
});
// Reactively adjust handle when setting changes
@ -980,6 +963,7 @@ BlazeComponent.extendComponent({
currentCardIsInThisList(listId, swimlaneId) {
return currentCardIsInThisList(listId, swimlaneId);
},
visible(list) {
if (list.archived) {
// Show archived list only when filter archive is on
@ -1003,7 +987,7 @@ BlazeComponent.extendComponent({
return true;
},
onRendered() {
const boardComponent = this.parentComponent();
let boardComponent = getBoardComponent();
const $listsDom = this.$('.js-lists');
@ -1015,25 +999,24 @@ BlazeComponent.extendComponent({
// Wait for DOM to be ready
setTimeout(() => {
const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles()
const handleSelector = Utils.isMiniScreen()
? '.js-list-handle'
: '.js-list-header';
: '.list-header-name-container';
const $lists = this.$('.js-list');
const $parent = $lists.parent();
const parent = $lists.parent();
if ($lists.length > 0) {
// Check for drag handles
const $handles = $parent.find('.js-list-handle');
const handles = $(parent).find(handleSelector);
// Test if drag handles are clickable
$handles.on('click', function(e) {
handles.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
});
$parent.sortable({
parent.sortable({
connectWith: '.js-swimlane, .js-lists',
tolerance: 'pointer',
appendTo: '.board-canvas',
@ -1045,18 +1028,25 @@ BlazeComponent.extendComponent({
disabled: !Utils.canModifyBoard(),
start(evt, ui) {
ui.helper.css('z-index', 1000);
ui.placeholder.height(ui.helper.height());
ui.placeholder.width(ui.helper.width());
width = ui.helper.width();
height = ui.helper.height();
ui.placeholder.height(height);
ui.placeholder.width(width);
ui.placeholder[0].setAttribute('style', `width: ${width}px !important; height: ${height}px !important;`);
EscapeActions.executeUpTo('popup-close');
boardComponent.setIsDragging(true);
},
stop(evt, ui) {
boardComponent.setIsDragging(false);
}
saveSorting(ui);
},
sort(event, ui) {
Utils.scrollIfNeeded(event);
},
});
// Reactively update handle when user toggles desktop drag handles
this.autorun(() => {
const newHandle = Utils.isTouchScreenOrShowDesktopDragHandles()
const newHandle = Utils.isMiniScreen()
? '.js-list-handle'
: '.js-list-header';
if ($parent.data('uiSortable') || $parent.data('sortable')) {

View file

@ -1,47 +1,40 @@
.member {
border-radius: 3px;
display: block;
position: relative;
float: left;
height: clamp(24px, 3.5vw, 36px);
width: clamp(24px, 3.5vw, 36px);
margin: .3vh;
cursor: pointer;
user-select: none;
z-index: 1;
text-decoration: none;
border-radius: 50%;
}
.member .avatar {
overflow: hidden;
border-radius: 50%;
}
.member .avatar.avatar-initials {
height: 70%;
width: 70%;
padding: 15%;
display: flex;
background-color: #dbdbdb;
color: #444;
position: absolute;
aspect-ratio: 1 / 1;
border-radius: 50%;
padding: 0.2em;
font-size: 0.9em;
height: var(--label-height);
align-items: center;
justify-content: center;
align-self: flex-start;
color: #111;
margin: 0 0.2ch;
}
.js-select-initials {
justify-content: start;
p {
margin: 0;
}
display: flex;
align-items: center;
justify-content: center;
}
.member .avatar.avatar-image {
object-fit: cover;
object-position: center;
height: 100%;
width: 100%;
}
.member .member-presence-status {
background-color: #b3b3b3;
border: 1px solid #fff;
border-radius: 50%;
height: 7px;
width: 7px;
height: 1.2ch;
width: 1.2ch;
position: absolute;
right: -1px;
bottom: -1px;
transform: translate(1.6ch, 1.6ch);
border: 1px solid #fff;
z-index: 15;
}
@ -61,18 +54,6 @@
background: #e44242;
border-color: #f1dada;
}
.member .edit-avatar {
position: absolute;
top: 0;
height: 100%;
width: 100%;
border-radius: 50%;
background: #000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
}
.member .edit-avatar:hover {
opacity: 0.6;
}
@ -112,9 +93,4 @@
}
.mini-profile-info .info p {
padding-top: 0;
}
.mini-profile-info .member {
width: clamp(40px, 5vw, 60px);
height: clamp(40px, 5vw, 60px);
margin-right: 10px;
}
}

View file

@ -19,8 +19,8 @@ template(name="userAvatar")
i.fa.fa-pencil-square-o
template(name="userAvatarInitials")
svg.avatar.avatar-initials(viewBox="0 0 {{viewPortWidth}} 15")
text(x="50%" y="11" text-anchor="middle" dominant-baseline="middle" font-size="16")= initials
.avatar-initials
= initials
template(name="orgAvatar")
a.member.orgOrTeamMember(class="js-member" title="{{orgData.orgDisplayName}}")

View file

@ -1,109 +1,106 @@
.auth-layout .at-form-landing-logo {
width: min(249px, 32vw);
margin: auto;
margin-top: 6vh;
margin-bottom: 2.5vh;
.auth-container {
display: grid;
align-content: stretch;
align-items: stretch;
justify-items: stretch;
justify-content: center;
padding: 2lh 0;
/* i.e. center horizontally */
margin-inline: auto;;
/* parent container has relative positionning */
grid-template-columns: 100%;
grid-template-rows: minmax(20vh, 300px) min-content 1fr;
position: relative;
}
body.mobile-mode:has(.auth-container) {
.auth-container {
grid-template-columns: 90vw;
min-height: 100%;
}
}
.auth-logo {
&, &>a:not(img), > img {
display: flex;
flex: 1;
justify-content: center;
}
}
.auth-container {
flex: 1;
max-width: max(30vw, 600px);
gap: 1lh;
margin-bottom: 1lh;
max-height: 80vh;
position: relative;
}
.auth-layout .auth-dialog {
width: min(275px, 36vw);
padding: 3vh 3vw;
margin: auto;
margin-bottom: 2.5vh;
background: #fff;
font-size: 1.1em;
border-radius: 0.4vw;
border: 1px solid #dbdbdb;
border-bottom-color: #c2c2c2;
box-shadow: 0 0.2vh 0.8vh rgba(0,0,0,0.3);
padding: 0 2ch 0.5lh 2ch;
white-space: wrap;
/* try to override properties of non-flex forms
without referring too much to classes and ids, as forms
are dynamic */
&, div:not(#legalNoticeDiv, .lds-roller, .password-input-container, :empty), form {
display: flex;
flex-direction: column;
gap: 1lh;
>:not(.at-input) {
gap: 0.4lh;
}
.at-input {
gap: 0;
}
}
*:not(div) {
width: 100%;
margin: 0;
}
}
.auth-layout .auth-dialog .at-form .at-link {
color: #17683a;
}
.auth-layout .auth-dialog .at-form label {
margin-bottom: 0.4vh;
}
.auth-layout .auth-dialog .at-form input {
width: 100%;
}
.password-input-container {
position: relative;
display: flex;
align-items: center;
display: grid;
align-self: stretch;
grid-template-columns: 1fr 6ch;
}
.password-input-container input {
flex: 1;
padding-right: 55px; /* More room for the bigger button */
box-sizing: border-box;
}
.password-toggle-btn {
position: absolute;
right: 5px; /* Adjusted for larger button */
top: calc(50% - 26px); /* Moved up by 20px + 6px = 26px total */
transform: translateY(-50%);
background: #f8f8f8 !important;
border: 1px solid #ddd !important;
border-radius: 3px !important;
color: #000 !important; /* Black color for the icon */
cursor: pointer;
padding: 8px 6px 8px 12px; /* 2x bigger padding, 6px less on right */
font-size: 16px; /* 2x bigger font size */
width: auto !important;
height: auto !important;
line-height: 1;
display: flex !important;
align-items: center;
justify-content: center;
z-index: 10;
min-width: 40px; /* 2x bigger minimum width */
min-height: 32px; /* 2x bigger minimum height */
}
/* Adjust position for login and register pages */
.auth-layout .password-toggle-btn {
top: calc(50% - 11px); /* Move 15px down for login/register */
}
.password-toggle-btn .eye-text {
color: #000 !important;
font-size: 16px !important;
line-height: 1;
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
opacity: 0.8;
}
.eye-slash-line {
position: absolute;
top: 10px;
left: 10px;
width: 20px;
height: 20px;
pointer-events: none;
stroke: #000;
stroke-width: 2;
fill: none;
}
.password-toggle-btn:hover .eye-text {
color: #000 !important;
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
opacity: 0.8;
body.mobile-mode {
.auth-layout {
max-height: unset;
}
.password-input-container {
grid-auto-flow: row;
}
}
.auth-layout .auth-dialog .at-form button {
width: 100%;
background: #216694;
color: #fff;
min-height: 2lh;
}
.auth-layout .auth-dialog .at-form .at-title {
background: #f7f7f7;
margin: -3vh -3vw;
padding: 2vh 3vw 0.7vh;
margin-bottom: 2.5vh;
border-bottom: 1px solid #dcdcdc;
color: #4d4d4d;
font-weight: bold;
text-align: center;
}
.auth-layout .auth-dialog .at-form .at-signup-link,
.auth-layout .auth-dialog .at-form .at-signin-link,
.auth-layout .auth-dialog .at-form .at-forgotPwd {
font-size: 0.9em;
margin-top: 2vh;
color: #4d4d4d;
}
.auth-layout .auth-dialog .at-form .at-signup-link .at-signUp,
@ -113,43 +110,4 @@
.auth-layout .auth-dialog .at-form .at-signin-link .at-signIn,
.auth-layout .auth-dialog .at-form .at-forgotPwd .at-signIn {
font-weight: bold;
}
.auth-layout .auth-dialog .at-form-lang {
margin-top: 0px;
}
.auth-layout .auth-dialog .at-form-lang .select-lang {
width: 100%;
margin-top: 10px;
}
@media screen and (max-width: 800px) {
.auth-layout {
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
}
.auth-layout .at-form-landing-logo {
width: 125px;
position: absolute;
top: 0px;
right: 20px;
margin-top: 5px;
margin-bottom: 5px;
}
.auth-layout .at-form-landing-logo img {
width: 125px;
}
.auth-layout .auth-dialog {
width: calc(100% - 50px);
height: calc(100% - 50px);
padding: 25px;
min-height: 380px;
margin: 0px;
margin-bottom: 0px;
border: 0px;
}
.auth-layout .auth-dialog .at-form .at-title h3 {
width: calc(100% - 125px);
overflow-x: hidden;
}
}
}

View file

@ -5,106 +5,126 @@ template(name="headerUserBar")
+userAvatar(userId=currentUser._id)
unless isMiniScreen
unless isSandstorm
if currentUser.profile.fullname
= currentUser.profile.fullname
else
= currentUser.username
.avatar-user-fullname
if currentUser.profile.fullname
= currentUser.profile.fullname
else
= currentUser.username
template(name="memberMenuPopup")
ul.pop-over-list
with currentUser
li
a.js-toggle-grey-icons(href="#")
i.fa.fa-paint-brush
| {{_ 'grey-icons'}}
span
i.fa.fa-paint-brush
| {{_ 'grey-icons'}}
if currentUser.profile
if currentUser.profile.GreyIcons
i.fa.fa-check
li
a.js-my-cards(href="{{pathFor 'my-cards'}}")
i.fa.fa-list
| {{_ 'my-cards'}}
span
i.fa.fa-list
| {{_ 'my-cards'}}
li
a.js-due-cards(href="{{pathFor 'due-cards'}}")
i.fa.fa-calendar
| {{_ 'dueCards-title'}}
span
i.fa.fa-calendar
| {{_ 'dueCards-title'}}
li
a.js-global-search(href="{{pathFor 'global-search'}}")
i.fa.fa-search
| {{_ 'globalSearch-title'}}
span
i.fa.fa-search
| {{_ 'globalSearch-title'}}
li
a(href="{{pathFor 'home'}}")
i.fa.fa-home
| {{_ 'all-boards'}}
span
i.fa.fa-home
| {{_ 'all-boards'}}
li
a(href="{{pathFor 'public'}}")
i.fa.fa-globe
| {{_ 'public'}}
span
i.fa.fa-globe
| {{_ 'public'}}
li
a.board-header-btn.js-open-archived-board
i.fa.fa-archive
span {{_ 'archives'}}
a.js-open-archived-board
span
i.fa.fa-archive
| {{_ 'archives'}}
li
a.js-notifications-drawer-toggle
i.fa.fa-bell
| {{_ 'notifications'}}
span
i.fa.fa-bell
| {{_ 'notifications'}}
if currentSetting.customHelpLinkUrl
li
a(href="{{currentSetting.customHelpLinkUrl}}", title="{{_ 'help'}}", target="_blank", rel="noopener noreferrer")
i.fa.fa-question-circle
| {{_ 'help'}}
span
i.fa.fa-question-circle
| {{_ 'help'}}
unless currentUser.isWorker
ul.pop-over-list
li
a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
i.fa.fa-list
| {{_ 'templates'}}
span
i.fa.fa-list
| {{_ 'templates'}}
if currentUser.isAdmin
li
a.js-go-setting(href="{{pathFor 'setting'}}")
i.fa.fa-lock
| {{_ 'admin-panel'}}
span
i.fa.fa-lock
| {{_ 'admin-panel'}}
hr
if isSameDomainNameSettingValue
li
a.js-invite-people
i.fa.fa-envelope
| {{_ 'invite-people'}}
span
i.fa.fa-envelope
| {{_ 'invite-people'}}
if isNotOAuth2AuthenticationMethod
li
a.js-edit-profile
i.fa.fa-user
| {{_ 'edit-profile'}}
span
i.fa.fa-user
| {{_ 'edit-profile'}}
li
a.js-change-settings
i.fa.fa-cog
| {{_ 'change-settings'}}
span
i.fa.fa-cog
| {{_ 'change-settings'}}
li
a.js-change-avatar
i.fa.fa-picture-o
| {{_ 'edit-avatar'}}
span
i.fa.fa-picture-o
| {{_ 'edit-avatar'}}
unless isSandstorm
if isNotOAuth2AuthenticationMethod
li
a.js-change-password
i.fa.fa-key
| {{_ 'changePasswordPopup-title'}}
span
i.fa.fa-key
| {{_ 'changePasswordPopup-title'}}
li
a.js-change-language
i.fa.fa-flag
| {{_ 'changeLanguagePopup-title'}}
span
i.fa.fa-flag
| {{_ 'changeLanguagePopup-title'}}
if isSupportPageEnabled
li
a(href="{{pathFor 'support'}}")
i.fa.fa-question-circle
| {{_ 'support'}}
span
i.fa.fa-question-circle
| {{_ 'support'}}
unless isSandstorm
hr
ul.pop-over-list
hr
li
a.js-logout
i.fa.fa-sign-out
| {{_ 'log-out'}}
span
i.fa.fa-sign-out
| {{_ 'log-out'}}
template(name="invitePeoplePopup")
ul#registration-setting.setting-detail
@ -134,7 +154,7 @@ template(name="editProfilePopup")
form
label
| {{_ 'fullname'}}
input.js-profile-fullname(type="text" value=profile.fullname autofocus)
input.js-profile-fullname(type="text" value=profile.fullname )
label
| {{_ 'username'}}
span.error.hide.username-taken

View file

@ -342,6 +342,7 @@ Template.changeLanguagePopup.events({
},
});
TAPi18n.setLanguage(this.tag);
Popup.close();
event.preventDefault();
},
});