mirror of
https://github.com/wekan/wekan.git
synced 2026-02-09 01:34:21 +01:00
Reverted New UI Design of WeKan v8.29 and added more fixes and performance improvements.
Thanks to xet7 !
This commit is contained in:
parent
d152d8fc1b
commit
1b8b8d2eef
196 changed files with 17659 additions and 10028 deletions
|
|
@ -1,22 +1,16 @@
|
|||
.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;
|
||||
font-size: 1.2em;
|
||||
width: 5vw;
|
||||
}
|
||||
.reactions-popup .add-comment-reaction:hover {
|
||||
background-color: #b0c4de;
|
||||
|
|
@ -24,20 +18,20 @@
|
|||
.activities {
|
||||
clear: both;
|
||||
}
|
||||
.activity {
|
||||
display: flex;
|
||||
}
|
||||
.activities .activity {
|
||||
margin: 0.1vh 0;
|
||||
padding: 0.8vh 0;
|
||||
display: flex;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.activities .activity .member {
|
||||
width: 4vw;
|
||||
height: 4vw;
|
||||
}
|
||||
.activities .activity .activity-member {
|
||||
font-weight: 700;
|
||||
}
|
||||
.activities .activity .activity-desc {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ Template.commentReactions.events({
|
|||
cardComment.toggleReaction(codepoint);
|
||||
}
|
||||
},
|
||||
'click .open-comment-reaction-popup': Popup.open('addReaction', {showHeader: false})
|
||||
'click .open-comment-reaction-popup': Popup.open('addReaction'),
|
||||
})
|
||||
|
||||
Template.addReactionPopup.events({
|
||||
|
|
@ -306,11 +306,6 @@ Template.addReactionPopup.helpers({
|
|||
'😊',
|
||||
'🤔',
|
||||
'😔'];
|
||||
},
|
||||
hasUserReacted(codepoint) {
|
||||
const commentId = Template.instance().data.commentId;
|
||||
const cardComment = ReactiveCache.getCardComment(commentId);
|
||||
return cardComment.hasUserReacted(codepoint);
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
.new-comment {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
gap: 1ch;
|
||||
margin: 0 0 20px 38px;
|
||||
}
|
||||
.new-comment .member {
|
||||
opacity: 0.7;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: -38px;
|
||||
}
|
||||
.new-comment.is-open .member {
|
||||
opacity: 1;
|
||||
|
|
@ -14,44 +14,34 @@
|
|||
.new-comment.is-open .helper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.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.is-open textarea {
|
||||
min-height: 100px;
|
||||
color: #4d4d4d;
|
||||
cursor: auto;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.new-comment .too-long {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
.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;
|
||||
}
|
||||
.comment-item {
|
||||
background-color: #fff;
|
||||
|
|
@ -75,30 +65,31 @@
|
|||
}
|
||||
.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 {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3lh;
|
||||
margin-left: 3px;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.comments .comment .comment-desc .comment-text {
|
||||
display: flex;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
text-decoration: none;
|
||||
|
|
@ -110,7 +101,6 @@
|
|||
display: flex;
|
||||
margin-top: 5px;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
.comments .comment .comment-desc .reactions .open-comment-reaction-popup {
|
||||
display: flex;
|
||||
|
|
@ -120,6 +110,7 @@
|
|||
}
|
||||
.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;
|
||||
|
|
@ -137,14 +128,10 @@
|
|||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,5 +64,5 @@ template(name="commentReactions")
|
|||
template(name="addReactionPopup")
|
||||
.reactions-popup
|
||||
each codepoint in codepoints
|
||||
unless (hasUserReacted codepoint)
|
||||
span.add-comment-reaction(data-codepoint="#{codepoint}") !{codepoint}
|
||||
span.add-comment-reaction(data-codepoint="#{codepoint}") !{codepoint}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
.board-conversion-modal {
|
||||
background: white;
|
||||
border-radius: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 0.4ch;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #2196F3, #21CBF3);
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
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: 0.4ch;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
.conversion-info {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +179,6 @@
|
|||
}
|
||||
|
||||
.board-conversion-header h3 {
|
||||
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,21 +6,21 @@ template(name="boardConversionProgress")
|
|||
i.fa.fa-cog
|
||||
| {{_ 'converting-board'}}
|
||||
p {{_ 'converting-board-description'}}
|
||||
|
||||
|
||||
.board-conversion-content
|
||||
.conversion-progress
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{conversionProgress}}%")
|
||||
.progress-text {{conversionProgress}}%
|
||||
|
||||
|
||||
.conversion-status
|
||||
i.fa.fa-cog
|
||||
| {{conversionStatus}}
|
||||
|
||||
|
||||
.conversion-time(style="{{#unless conversionEstimatedTime}}display: none;{{/unless}}")
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'estimated-time-remaining'}}: {{conversionEstimatedTime}}
|
||||
|
||||
|
||||
.board-conversion-footer
|
||||
.conversion-info
|
||||
i.fa.fa-info-circle
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import {
|
||||
import {
|
||||
boardConverter,
|
||||
isConverting,
|
||||
conversionProgress,
|
||||
|
|
@ -12,15 +12,15 @@ Template.boardConversionProgress.helpers({
|
|||
isConverting() {
|
||||
return isConverting.get();
|
||||
},
|
||||
|
||||
|
||||
conversionProgress() {
|
||||
return conversionProgress.get();
|
||||
},
|
||||
|
||||
|
||||
conversionStatus() {
|
||||
return conversionStatus.get();
|
||||
},
|
||||
|
||||
|
||||
conversionEstimatedTime() {
|
||||
return conversionEstimatedTime.get();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,25 +1,43 @@
|
|||
.swim-flex {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding-bottom: 40vw;
|
||||
}
|
||||
|
||||
.board-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
/* don't stretch vertically if not needed (e.g collapsed) */
|
||||
align-self: start;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure horizontal scrollbar is visible for high zoom levels */
|
||||
|
|
@ -79,12 +97,172 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
/* Force vertical scrollbar to always be visible */
|
||||
#content[style*="overflow-y: auto"] {
|
||||
overflow-y: scroll !important;
|
||||
#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;
|
||||
}
|
||||
|
||||
/* 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"] {
|
||||
|
|
@ -96,6 +274,36 @@
|
|||
#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 {
|
||||
|
|
@ -112,14 +320,73 @@ 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 .swimlane {
|
||||
/* this effectively prevents board
|
||||
to shrink */
|
||||
min-width: 100vw;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.calendar-event-green {
|
||||
|
|
@ -278,6 +545,7 @@ body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
|
|
@ -290,6 +558,10 @@ 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
template(name="board")
|
||||
|
||||
if isConverting.get
|
||||
+boardConversionProgress
|
||||
else if isBoardReady.get
|
||||
if currentBoard
|
||||
+boardBody
|
||||
if onlyShowCurrentCard
|
||||
+cardDetails(currentCard)
|
||||
else
|
||||
+boardBody
|
||||
else
|
||||
//-- XXX We need a better error message in case the board has been archived
|
||||
+message(label="board-not-found")
|
||||
|
|
@ -13,32 +17,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
|
||||
.swim-flex
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
else if isViewSwimlanes
|
||||
if hasSwimlanes
|
||||
each currentBoard.swimlanes
|
||||
+swimlane(this)
|
||||
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
|
||||
// Fallback: If no swimlanes exist, show lists instead of empty message
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewLists
|
||||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
|
|
@ -52,6 +56,10 @@ 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")
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ BlazeComponent.extendComponent({
|
|||
this.isBoardReady.set(true); // Show board even if conversion check failed
|
||||
}
|
||||
},
|
||||
|
||||
onlyShowCurrentCard() {
|
||||
const isMiniScreen = Utils.isMiniScreen();
|
||||
const currentCardId = Utils.getCurrentCardId(true);
|
||||
|
|
@ -113,7 +114,7 @@ BlazeComponent.extendComponent({
|
|||
|
||||
openCards() {
|
||||
// In desktop mode, return array of all open cards
|
||||
const isMobile = Utils.isMiniScreen();
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCardIds = Session.get('openCards') || [];
|
||||
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
|
||||
|
|
@ -122,7 +123,7 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
goHome() {
|
||||
FlowRouter.go('home')
|
||||
FlowRouter.go('home');
|
||||
},
|
||||
|
||||
isConverting() {
|
||||
|
|
@ -194,7 +195,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
onRendered() {
|
||||
// Initialize user settings (mobile mode)
|
||||
// Initialize user settings (zoom and mobile mode)
|
||||
Utils.initializeUserSettings();
|
||||
|
||||
// Detect iPhone devices and add class for better CSS targeting
|
||||
|
|
@ -390,24 +391,23 @@ BlazeComponent.extendComponent({
|
|||
helper(evt, item) {
|
||||
const helper = $(`<div class="swimlane"
|
||||
style="flex-direction: column;
|
||||
max-height: 30vh;
|
||||
width: 100vw;
|
||||
overflow: hidden; z-index: 100;"/>`);
|
||||
height: ${swimlaneWhileSortingHeight}px;
|
||||
width: $(boardComponent.width)px;
|
||||
overflow: hidden;"/>`);
|
||||
helper.append(item.clone());
|
||||
// Also grab the list of lists of cards
|
||||
const list = item.next();
|
||||
helper.append(list.clone());
|
||||
return helper;
|
||||
},
|
||||
items: '.swimlane-container',
|
||||
items: '.swimlane:not(.placeholder)',
|
||||
placeholder: 'swimlane placeholder',
|
||||
distance: 7,
|
||||
start(evt, ui) {
|
||||
const listDom = ui.placeholder.next('.js-swimlane');
|
||||
const parentOffset = ui.item.parent().offset();
|
||||
|
||||
height = ui.helper.height();
|
||||
ui.placeholder[0].setAttribute('style', `height: ${height}px !important;`);
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
EscapeActions.executeUpTo('popup-close');
|
||||
listDom.addClass('moving-swimlane');
|
||||
boardComponent.setIsDragging(true);
|
||||
|
|
@ -415,19 +415,40 @@ 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('.swimlane-container').get(0);
|
||||
const nextSwimlaneDom = ui.item.nextAll('.swimlane-container').get(0);
|
||||
const prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0);
|
||||
const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0);
|
||||
const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);
|
||||
|
||||
$swimlanesDom.sortable('cancel');
|
||||
|
|
@ -443,7 +464,39 @@ BlazeComponent.extendComponent({
|
|||
boardComponent.setIsDragging(false);
|
||||
},
|
||||
sort(evt, ui) {
|
||||
Utils.scrollIfNeeded(evt);
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -452,10 +505,10 @@ BlazeComponent.extendComponent({
|
|||
dragscroll.reset();
|
||||
|
||||
if ($swimlanesDom.data('uiSortable') || $swimlanesDom.data('sortable')) {
|
||||
if (Utils.isMiniScreen()) {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$swimlanesDom.sortable('option', 'handle', '.js-swimlane-header-handle');
|
||||
} else {
|
||||
$swimlanesDom.sortable('option', 'handle', '.swimlane-header-wrap');
|
||||
$swimlanesDom.sortable('option', 'handle', '.swimlane-header');
|
||||
}
|
||||
|
||||
// Disable drag-dropping if the current user is not a board member
|
||||
|
|
@ -972,3 +1025,4 @@ 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
|
|
@ -22,90 +22,918 @@
|
|||
padding: 0.7vh 0.7vw;
|
||||
}
|
||||
|
||||
.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 {
|
||||
/* Zoom and Mobile Mode Controls */
|
||||
.board-header-btns.center {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1ch;
|
||||
& p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.board-header-btns-right > a {
|
||||
flex-wrap: no-wrap;
|
||||
/* Hide desktop-only elements in mobile mode (like mobile media queries do) */
|
||||
.mobile-mode .board-header-btn i.fa + span {
|
||||
display: none !important;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
.mobile-mode .board-header-btn span {
|
||||
display: none !important;
|
||||
}
|
||||
.board-header-btns-left {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 2ch;
|
||||
padding: 0 0.5ch;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,26 @@
|
|||
template(name="boardHeaderBar")
|
||||
.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}}
|
||||
h1.header-board-menu
|
||||
with currentBoard
|
||||
if $eq title 'Templates'
|
||||
| {{_ 'templates'}}
|
||||
else
|
||||
+viewer
|
||||
= title
|
||||
|
||||
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
|
||||
.board-header-btns.left
|
||||
unless isMiniScreen
|
||||
if currentBoard
|
||||
if isMiniScreen
|
||||
if currentUser
|
||||
with 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
|
||||
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(
|
||||
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.js-watch-board(
|
||||
title="{{_ watchLevel }}")
|
||||
|
|
@ -70,43 +29,89 @@ 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.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
if isSandstorm
|
||||
if currentUser
|
||||
a.board-header-btn.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
|
||||
|
|
@ -138,9 +143,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
|
||||
|
|
@ -170,29 +175,26 @@ template(name="boardChangeWatchPopup")
|
|||
li
|
||||
with "watching"
|
||||
a.js-select-watch
|
||||
span
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watching'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
i.fa.fa-eye
|
||||
| {{_ 'watching'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'watching-info'}}
|
||||
li
|
||||
with "tracking"
|
||||
a.js-select-watch
|
||||
span
|
||||
i.fa.fa-bell
|
||||
| {{_ 'tracking'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
i.fa.fa-bell
|
||||
| {{_ 'tracking'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'tracking-info'}}
|
||||
li
|
||||
with "muted"
|
||||
a.js-select-watch
|
||||
span
|
||||
i.fa.fa-bell-slash
|
||||
| {{_ 'muted'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
i.fa.fa-bell-slash
|
||||
| {{_ 'muted'}}
|
||||
if watchCheck
|
||||
i.fa.fa-check
|
||||
span.sub-name {{_ 'muted-info'}}
|
||||
|
||||
template(name="boardChangeViewPopup")
|
||||
|
|
@ -248,13 +250,12 @@ template(name="createBoard")
|
|||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
.create-element-foooter
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
|
||||
template(name="createBoardPopup")
|
||||
form
|
||||
|
|
@ -278,13 +279,12 @@ template(name="createBoardPopup")
|
|||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
input.primary.wide(type="submit" value="{{_ 'create'}}")
|
||||
.create-element-foooter
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-import-board {{_ 'import'}}
|
||||
span.quiet
|
||||
| /
|
||||
a.js-board-template {{_ 'template'}}
|
||||
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")
|
||||
|
|
@ -308,28 +308,39 @@ template(name="createTemplateContainerPopup")
|
|||
a.flex.js-toggle-add-template-container
|
||||
.materialCheckBox#add-template-container
|
||||
span {{_ 'add-template-container'}}
|
||||
.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'}}
|
||||
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
|
||||
// | {{_ 'list-sort-by'}}
|
||||
// hr
|
||||
// ul.pop-over-list
|
||||
// each value in allowedSortValues
|
||||
// li
|
||||
// a.js-sort-by(name="{{value.name}}")
|
||||
// if $eq sortby value.name
|
||||
// | {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
// | {{_ value.label }}{{_ value.shortLabel}}
|
||||
// if $eq sortby value.name
|
||||
// i.fa.fa-check
|
||||
//
|
||||
h2
|
||||
//
|
||||
| {{_ 'list-sort-by'}}
|
||||
//
|
||||
hr
|
||||
//
|
||||
ul.pop-over-list
|
||||
//
|
||||
each value in allowedSortValues
|
||||
//
|
||||
li
|
||||
//
|
||||
a.js-sort-by(name="{{value.name}}")
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
| {{#if $eq Direction "fa-arrow-up"}}⬆️{{else}}⬇️{{/if}}
|
||||
//
|
||||
| {{_ value.label }}{{_ value.shortLabel}}
|
||||
//
|
||||
if $eq sortby value.name
|
||||
//
|
||||
i.fa.fa-check
|
||||
template(name="boardChangeTitlePopup")
|
||||
form
|
||||
label
|
||||
|
|
@ -366,3 +377,4 @@ template(name="cardsSortPopup")
|
|||
a.js-sort-created-asc
|
||||
i.fa.fa-arrow-up
|
||||
| {{_ 'created-at-oldest-first'}}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ BlazeComponent.extendComponent({
|
|||
const currentBoard = Utils.getCurrentBoard();
|
||||
return currentBoard && currentBoard.getWatchLevel(Meteor.userId());
|
||||
},
|
||||
|
||||
|
||||
|
||||
isStarred() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -67,71 +67,81 @@ template(name="boardList")
|
|||
// Right boards grid
|
||||
.boards-right-grid
|
||||
.boards-path-header
|
||||
.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}}")
|
||||
.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
|
||||
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-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
|
||||
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
|
||||
| {{_ 'add-template-container'}}
|
||||
| {{_ 'add-template-container'}}
|
||||
else
|
||||
a.board-list-item.label(title="{{_ 'add-board'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-plus
|
||||
| {{_ '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
|
||||
.board-card-header
|
||||
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
|
||||
span.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}}")
|
||||
.board-card-body
|
||||
span.details
|
||||
span.board-list-item-name= title
|
||||
| {{#if isStarred}}⭐{{else}}☆{{/if}}
|
||||
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"
|
||||
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}}")
|
||||
|
||||
.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}}")
|
||||
span.details
|
||||
span.board-list-item-name(title="{{_ 'template-container'}}")
|
||||
+viewer
|
||||
= title
|
||||
//- #FIXME: is this obsolete ?
|
||||
//- p.board-list-item-desc
|
||||
//- +viewer
|
||||
//- = description
|
||||
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}}"
|
||||
|
|
@ -144,20 +154,19 @@ template(name="boardList")
|
|||
span.emoji-icon
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
else
|
||||
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}}")
|
||||
|
||||
.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}}")
|
||||
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
|
||||
|
|
@ -166,24 +175,40 @@ template(name="boardList")
|
|||
+userAvatar(userId=member noRemove=true)
|
||||
unless currentSetting.hideCardCounterList
|
||||
if allowsCardCounterList
|
||||
.minicard-lists
|
||||
.minicard-lists.flex.flex-wrap
|
||||
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")
|
||||
|
|
@ -212,16 +237,3 @@ 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'}}
|
||||
|
|
@ -108,7 +108,10 @@ 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
|
||||
|
|
@ -124,39 +127,46 @@ 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)';
|
||||
|
||||
// #FIXME OLD SORTABLE CODE - WILL BE DISABLED
|
||||
//
|
||||
// const itemsSelector = '.js-board';
|
||||
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);
|
||||
|
||||
// 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);
|
||||
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 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);
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
this.autorun(() => {
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$boards.sortable({
|
||||
handle: '.board-handle',
|
||||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
},
|
||||
userHasTeams() {
|
||||
if (ReactiveCache.getCurrentUser()?.teams?.length > 0) return true;
|
||||
|
|
@ -347,7 +357,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}: ${cardCountcardCount}`;
|
||||
return `${list.title}: ${cardCount}`;
|
||||
});
|
||||
return ret;
|
||||
*/
|
||||
|
|
@ -525,7 +535,6 @@ BlazeComponent.extendComponent({
|
|||
'click .js-multiselection-reset'(evt) {
|
||||
evt.preventDefault();
|
||||
BoardMultiSelection.disable();
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-toggle-board-multi-selection'(evt) {
|
||||
evt.preventDefault();
|
||||
|
|
@ -699,7 +708,6 @@ BlazeComponent.extendComponent({
|
|||
icon: newIcon || '📁',
|
||||
});
|
||||
|
||||
|
||||
Meteor.call('setWorkspacesTree', updatedTree, (err) => {
|
||||
if (err) console.error(err);
|
||||
});
|
||||
|
|
@ -800,7 +808,6 @@ 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
|
||||
|
|
@ -823,7 +830,6 @@ 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') {
|
||||
|
|
@ -838,11 +844,9 @@ 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;
|
||||
|
||||
|
|
@ -904,7 +908,6 @@ BlazeComponent.extendComponent({
|
|||
};
|
||||
const allBoards = ReactiveCache.getBoards(query, {});
|
||||
|
||||
|
||||
if (type === 'starred') {
|
||||
return allBoards.filter(
|
||||
(b) => currentUser && currentUser.hasStarred(b._id),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
.original-positions-content {
|
||||
background-color: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
.original-position-item {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<i class="fa fa-history"></i>
|
||||
{{#if isShowingOriginalPositions}}Hide{{else}}Show{{/if}} Original Positions
|
||||
</button>
|
||||
|
||||
|
||||
{{#if isShowingOriginalPositions}}
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="{{refreshHistory}}">
|
||||
<i class="fa fa-refresh"></i> Refresh
|
||||
|
|
@ -22,22 +22,22 @@
|
|||
{{else}}
|
||||
<div class="original-positions-filters">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'all'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'all'}}">
|
||||
All
|
||||
</button>
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'swimlane'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'swimlane'}}">
|
||||
<i class="fa fa-bars"></i> Swimlanes
|
||||
</button>
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'list'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'list'}}">
|
||||
<i class="fa fa-columns"></i> Lists
|
||||
</button>
|
||||
<button type="button"
|
||||
<button type="button"
|
||||
class="btn {{#if isFilterType 'card'}}btn-primary{{else}}btn-outline-secondary{{/if}}"
|
||||
onclick="{{setFilterType 'card'}}">
|
||||
<i class="fa fa-sticky-note"></i> Cards
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
if (!boardId) return;
|
||||
|
||||
this.isLoading.set(true);
|
||||
|
||||
|
||||
Meteor.call('positionHistory.getBoardHistory', boardId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
if (error) {
|
||||
|
|
@ -57,11 +57,11 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
getFilteredHistory() {
|
||||
const history = this.getBoardHistory();
|
||||
const filterType = this.filterType.get();
|
||||
|
||||
|
||||
if (filterType === 'all') {
|
||||
return history;
|
||||
}
|
||||
|
||||
|
||||
return history.filter(item => item.entityType === filterType);
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
getEntityOriginalPositionDescription(entity) {
|
||||
const position = entity.originalPosition || {};
|
||||
let description = `Position: ${position.sort || 0}`;
|
||||
|
||||
|
||||
if (entity.entityType === 'list' && entity.originalSwimlaneId) {
|
||||
description += ` in swimlane ${entity.originalSwimlaneId}`;
|
||||
} else if (entity.entityType === 'card') {
|
||||
|
|
@ -104,7 +104,7 @@ class OriginalPositionsViewComponent extends BlazeComponent {
|
|||
description += ` in list ${entity.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,80 +6,75 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
.attachment-gallery {
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.attachment-item {
|
||||
display: grid;
|
||||
grid-template-columns: 10ch auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
grid-template-rows: repeat(auto-fit, minmax(1.5lh, auto));
|
||||
justify-content: stretch;
|
||||
gap: 2ch;
|
||||
padding: 2ch;
|
||||
border-radius: 0.6ch;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.attachment-item:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.attachment-details-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.attachment-thumbnail-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
max-height: 150px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.attachment-thumbnail {
|
||||
/* more deterministic outcome */
|
||||
aspect-ratio: 1/1;
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
min-height: 2em;
|
||||
cursor: pointer;
|
||||
border-radius: 0.4ch;
|
||||
}
|
||||
.attachment-thumbnail-text {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
min-height: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2em;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.attachment-details-container {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.attachment-details {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 0.5ch;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-right: 25px; /* Make sure the icons are not to far to the right */
|
||||
}
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1.5ch;
|
||||
}
|
||||
|
||||
body.mobile-mode .attachment-actions {
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
.attachment-actions a {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.attachment-actions a:first-child {
|
||||
margin-left: 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;
|
||||
|
|
@ -100,25 +95,26 @@ body.mobile-mode .attachment-actions {
|
|||
height: 100%;
|
||||
}
|
||||
#viewer-top-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
font-size: 2rem;
|
||||
padding: 0.3lh 0.5ch;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
#attachment-name {
|
||||
color: white;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: 1.5em;
|
||||
max-width: calc(
|
||||
100% - 50px
|
||||
); /* Make sure the name does not overlap the close button */
|
||||
}
|
||||
#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 */
|
||||
|
|
@ -126,24 +122,30 @@ body.mobile-mode .attachment-actions {
|
|||
.card-details-upload-progress {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.4ch;
|
||||
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
@ -156,17 +158,22 @@ body.mobile-mode .attachment-actions {
|
|||
|
||||
.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;
|
||||
|
|
@ -180,6 +187,7 @@ body.mobile-mode .attachment-actions {
|
|||
.upload-progress-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +199,47 @@ body.mobile-mode .attachment-actions {
|
|||
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;
|
||||
|
|
@ -207,6 +256,7 @@ body.mobile-mode .attachment-actions {
|
|||
color: white;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
margin: 0 20px;
|
||||
}
|
||||
#prev-attachment {
|
||||
font-size: 4em;
|
||||
|
|
@ -272,6 +322,7 @@ body.mobile-mode .attachment-actions {
|
|||
position: absolute;
|
||||
bottom: 2.2em;
|
||||
font-size: 1.6em;
|
||||
padding: 16px;
|
||||
}
|
||||
#prev-attachment {
|
||||
left: 0;
|
||||
|
|
@ -305,10 +356,19 @@ body.mobile-mode .attachment-actions {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,11 +49,15 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatDateByUserPreference,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
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 { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
.card-date {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
background-color: #dbdbdb;
|
||||
}
|
||||
|
|
@ -105,10 +106,6 @@
|
|||
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 */
|
||||
|
|
@ -142,6 +139,6 @@
|
|||
}
|
||||
.customfield-date {
|
||||
display: block;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
.new-description {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
.new-description textarea {
|
||||
min-height: 1lh;
|
||||
.new-description.is-open .helper {
|
||||
display: inline-block;
|
||||
}
|
||||
.new-description.is-open textarea {
|
||||
min-height: 100px;
|
||||
color: #4d4d4d;
|
||||
cursor: auto;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.new-description .too-long {
|
||||
margin-top: 8px;
|
||||
|
|
@ -15,6 +19,9 @@
|
|||
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,
|
||||
|
|
@ -32,12 +39,16 @@
|
|||
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
|
|
@ -35,7 +35,6 @@ 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;
|
||||
|
|
@ -61,8 +60,19 @@ 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');
|
||||
|
|
@ -75,18 +85,6 @@ 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;
|
||||
|
|
@ -97,8 +95,8 @@ BlazeComponent.extendComponent({
|
|||
return ReactiveCache.getCurrentUser().hasCustomFieldsGrid();
|
||||
},
|
||||
|
||||
|
||||
cardMaximized() {
|
||||
this.dep.depend();
|
||||
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
|
||||
},
|
||||
|
||||
|
|
@ -177,11 +175,6 @@ 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();
|
||||
|
|
@ -216,11 +209,11 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
|
||||
const $checklistsDom = this.$('.card-checklist-items');
|
||||
const sortableSelector = Utils.isMiniScreen() ? '.checklist-handle' : '.checklist-title';
|
||||
|
||||
$checklistsDom.sortable({
|
||||
tolerance: 'pointer',
|
||||
helper: 'clone',
|
||||
handle: sortableSelector,
|
||||
handle: '.checklist-title',
|
||||
items: '.js-checklist',
|
||||
placeholder: 'checklist placeholder',
|
||||
distance: 7,
|
||||
|
|
@ -289,8 +282,6 @@ BlazeComponent.extendComponent({
|
|||
return ReactiveCache.getCurrentUser()?.isBoardMember();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Disable sorting if the current user is not a board member
|
||||
this.autorun(() => {
|
||||
const disabled = !userIsMember();
|
||||
|
|
@ -298,7 +289,10 @@ BlazeComponent.extendComponent({
|
|||
$checklistsDom.data('uiSortable') ||
|
||||
$checklistsDom.data('sortable')
|
||||
) {
|
||||
$checklistsDom.sortable('option', 'handle', sortableSelector);
|
||||
$checklistsDom.sortable('option', 'disabled', disabled);
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$checklistsDom.sortable({ handle: '.checklist-handle' });
|
||||
}
|
||||
}
|
||||
if ($subtasksDom.data('uiSortable') || $subtasksDom.data('sortable')) {
|
||||
$subtasksDom.sortable('option', 'disabled', disabled);
|
||||
|
|
@ -307,7 +301,11 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
onDestroyed() {
|
||||
this.boardBody?.showOverlay.set(false);
|
||||
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);
|
||||
},
|
||||
|
||||
events() {
|
||||
|
|
@ -334,11 +332,59 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
'mousedown .js-card-drag-handle'(event) {
|
||||
event.preventDefault();
|
||||
PopupComponent.toFront(event);
|
||||
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);
|
||||
},
|
||||
'click .js-card-send-to-back'(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
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
PopupComponent.toBack(event);
|
||||
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);
|
||||
},
|
||||
'click .js-close-card-details'() {
|
||||
// Get board ID from either the card data or current board in session
|
||||
|
|
@ -346,21 +392,26 @@ BlazeComponent.extendComponent({
|
|||
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
|
||||
const cardId = card && card._id;
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// 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
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Mobile mode: Clear the current card session to close the card
|
||||
Session.set('currentCard', null);
|
||||
|
||||
// Navigate back to board without card
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (board) {
|
||||
FlowRouter.go('board', {
|
||||
|
|
@ -383,6 +434,34 @@ 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();
|
||||
|
|
@ -446,7 +525,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'(event) {Popup.open('cardLabels')(event, { dataContextIfCurrentDataIsUndefined: this.currentData() })},
|
||||
'click .js-add-labels': Popup.open('cardLabels'),
|
||||
'click .js-received-date': Popup.open('editCardReceivedDate'),
|
||||
'click .js-start-date': Popup.open('editCardStartDate'),
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
|
|
@ -455,10 +534,12 @@ BlazeComponent.extendComponent({
|
|||
'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
|
||||
'click .js-custom-fields': Popup.open('cardCustomFields'),
|
||||
'mouseenter .js-card-details'() {
|
||||
if (this.boardBody) {
|
||||
this.boardBody.showOverlay.set(true);
|
||||
this.boardBody.mouseHasEnterCardDetails = true;
|
||||
}
|
||||
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;
|
||||
},
|
||||
'mousedown .js-card-details'() {
|
||||
Session.set('cardDetailsIsDragging', false);
|
||||
|
|
@ -479,13 +560,13 @@ BlazeComponent.extendComponent({
|
|||
'click #toggleCustomFieldsGridButton'() {
|
||||
Meteor.call('toggleCustomFieldsGrid');
|
||||
},
|
||||
'click .js-maximize-card-details'(e) {
|
||||
PopupComponent.maximize(e);
|
||||
'click .js-maximize-card-details'() {
|
||||
Meteor.call('toggleCardMaximized');
|
||||
autosize($('.card-details'));
|
||||
},
|
||||
'click .js-minimize-card-details'(e) {
|
||||
PopupComponent.minimize(e);
|
||||
'click .js-minimize-card-details'() {
|
||||
Meteor.call('toggleCardMaximized');
|
||||
autosize($('.card-details'));
|
||||
},
|
||||
'click .js-vote'(e) {
|
||||
const forIt = $(e.target).hasClass('js-vote-positive');
|
||||
|
|
@ -656,6 +737,16 @@ 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() {
|
||||
|
|
@ -792,7 +883,9 @@ 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)
|
||||
Meteor.call('watch', 'card', currentCard._id, level, (err, ret) => {
|
||||
if (!err && ret) Popup.close();
|
||||
});
|
||||
},
|
||||
'click .js-toggle-show-list-on-minicard'() {
|
||||
const currentCard = this;
|
||||
|
|
@ -803,6 +896,9 @@ Template.cardDetailsActionsPopup.events({
|
|||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onRendered() {
|
||||
autosize(this.$('textarea.js-edit-card-title'));
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -883,6 +979,10 @@ 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
|
||||
|
|
@ -892,6 +992,10 @@ 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
|
||||
|
|
@ -1801,6 +1905,9 @@ EscapeActions.register(
|
|||
() => {
|
||||
return !Session.equals('currentCard', null);
|
||||
},
|
||||
{
|
||||
noClickEscapeOn: '.js-card-details,.board-sidebar,#header',
|
||||
},
|
||||
);
|
||||
|
||||
Template.cardAssigneesPopup.onCreated(function () {
|
||||
|
|
@ -1878,16 +1985,3 @@ 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"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
.card-time {
|
||||
display: block;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
color: #fff;
|
||||
background-color: #dbdbdb;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
textarea.js-add-checklist-item,
|
||||
textarea.js-edit-checklist-item {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-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;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
float: right;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
|
@ -25,7 +25,6 @@ textarea.js-edit-checklist-item {
|
|||
.checklists-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.checklist-progress-bar-container {
|
||||
display: flex;
|
||||
|
|
@ -36,7 +35,7 @@ textarea.js-edit-checklist-item {
|
|||
margin-right: 10px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
flex: 1;
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 16px;
|
||||
|
|
@ -48,29 +47,19 @@ textarea.js-edit-checklist-item {
|
|||
border-radius: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.checklist-controls {
|
||||
display: flex;
|
||||
gap: 0.25lh;
|
||||
}
|
||||
|
||||
.checklist-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.checklist-title .checkbox {
|
||||
float: left;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.checklist-title p, .title {
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
.checklist-title .title {
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.checklist-title .checklist-stat {
|
||||
margin: 0 0.5em;
|
||||
|
|
@ -90,31 +79,29 @@ textarea.js-edit-checklist-item {
|
|||
bottom: -600px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.checklist {
|
||||
padding: 0.5lh;
|
||||
margin: 0.5lh 0;
|
||||
background-color: #f7f7f7;
|
||||
background: #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;
|
||||
gap: 0.25lh;
|
||||
background: #f7f7f7;
|
||||
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;
|
||||
|
|
@ -127,21 +114,26 @@ 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: 0.2ch solid #3cb500;
|
||||
border-right: 0.2ch solid #3cb500;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
.checklist-item .item-title {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
flex: 1;
|
||||
cursor: grab;
|
||||
}
|
||||
.checklist-item .item-title.is-checked {
|
||||
color: #8c8c8c;
|
||||
|
|
@ -149,18 +141,27 @@ textarea.js-edit-checklist-item {
|
|||
text-decoration: line-through;
|
||||
}
|
||||
.checklist-item .item-title .viewer p {
|
||||
display: flex;
|
||||
overflow-wrap: break-word;
|
||||
margin-bottom: 2px;
|
||||
display: block;
|
||||
word-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: inline-block;
|
||||
display: block;
|
||||
width: 50%;
|
||||
}
|
||||
.add-checklist-item.js-open-inlined-form:hover,
|
||||
.add-checklist.js-open-inlined-form:hover {
|
||||
|
|
@ -168,13 +169,25 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ template(name="checklists")
|
|||
i.fa.fa-check
|
||||
| {{_ 'checklists'}}
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId position="top")
|
||||
+inlinedForm(autoclose=false classNames="js-add-checklist" cardId = cardId
|
||||
position="top")
|
||||
+addChecklistItemForm
|
||||
else
|
||||
a.add-checklist-top.js-open-inlined-form(title="{{_ 'add-checklist'}}")
|
||||
|
|
@ -36,31 +37,26 @@ template(name="checklistDetail")
|
|||
+editChecklistItemForm(checklist = checklist)
|
||||
else
|
||||
.checklist-title
|
||||
h4.title
|
||||
if canModifyCard
|
||||
a.js-open-inlined-form.is-editable(title="{{_ 'moveChecklistPopup-title'}}")
|
||||
+viewer
|
||||
= checklist.title
|
||||
else
|
||||
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'}}")
|
||||
+viewer
|
||||
= checklist.title
|
||||
.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
|
||||
else
|
||||
h4.title
|
||||
+viewer
|
||||
= checklist.title
|
||||
|
||||
//- 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
|
||||
if $gt finishedPercent 0
|
||||
.checklist-progress-bar-container
|
||||
.checklist-progress-text {{finishedPercent}}%
|
||||
.checklist-progress-bar
|
||||
.checklist-progress(style="width:{{finishedPercent}}%")
|
||||
else
|
||||
.checklist-progress(style="visibility:hidden")
|
||||
+checklistItems(checklist = checklist card = card)
|
||||
|
||||
template(name="checklistDeletePopup")
|
||||
|
|
@ -69,7 +65,7 @@ template(name="checklistDeletePopup")
|
|||
|
||||
template(name="addChecklistItemForm")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip.copied-tooltip-hidden {{_ 'copied'}}
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-add-checklist-item(rows='1' autofocus)
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
|
||||
|
|
@ -78,12 +74,16 @@ 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-tooltip-hidden {{_ 'copied'}}
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
|
||||
if $eq type 'item'
|
||||
= item.title
|
||||
|
|
@ -100,6 +100,13 @@ 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)
|
||||
|
|
@ -112,15 +119,14 @@ 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 isMiniScreen
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
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
|
||||
|
|
@ -136,16 +142,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")
|
||||
|
|
@ -153,7 +159,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
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { DialogWithBoardSwimlaneListCard } from '/client/lib/dialogWithBoardSwim
|
|||
const subManager = new SubsManager();
|
||||
const { calculateIndexData, capitalize } = Utils;
|
||||
|
||||
function initSorting(items, handleSelector) {
|
||||
function initSorting(items) {
|
||||
items.sortable({
|
||||
tolerance: 'pointer',
|
||||
helper: 'clone',
|
||||
|
|
@ -16,7 +16,6 @@ function initSorting(items, handleSelector) {
|
|||
appendTo: 'parent',
|
||||
distance: 7,
|
||||
placeholder: 'checklist-item placeholder',
|
||||
handle: handleSelector,
|
||||
scroll: true,
|
||||
start(evt, ui) {
|
||||
ui.placeholder.height(ui.helper.height());
|
||||
|
|
@ -49,9 +48,8 @@ function initSorting(items, handleSelector) {
|
|||
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, this.handleSelector);
|
||||
initSorting(self.itemsDom);
|
||||
self.itemsDom.mousedown(function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
|
|
@ -65,9 +63,11 @@ BlazeComponent.extendComponent({
|
|||
const $itemsDom = $(self.itemsDom);
|
||||
if ($itemsDom.data('uiSortable') || $itemsDom.data('sortable')) {
|
||||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
$(self.itemsDom).sortable({
|
||||
handle: this.handleSelector,
|
||||
});
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(self.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -372,9 +372,9 @@ BlazeComponent.extendComponent({
|
|||
const ret = ReactiveCache.getCurrentUser().getMoveChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setMoveChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.move(cardId);
|
||||
await this.data().checklist.move(cardId);
|
||||
}
|
||||
}).register('moveChecklistPopup');
|
||||
|
||||
|
|
@ -384,8 +384,8 @@ BlazeComponent.extendComponent({
|
|||
const ret = ReactiveCache.getCurrentUser().getCopyChecklistDialogOptions();
|
||||
return ret;
|
||||
}
|
||||
setDone(cardId, options) {
|
||||
async setDone(cardId, options) {
|
||||
ReactiveCache.getCurrentUser().setCopyChecklistDialogOption(this.currentBoardId, options);
|
||||
this.data().checklist.copy(cardId);
|
||||
await this.data().checklist.copy(cardId);
|
||||
}
|
||||
}).register('copyChecklistPopup');
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
.card-label {
|
||||
border-radius: 0.4ch;
|
||||
border: 1px solid #000;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-weight: 700;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
.card-label:hover {
|
||||
color: #fff;
|
||||
|
|
@ -34,7 +34,6 @@
|
|||
}
|
||||
.card-label p {
|
||||
margin: 0px;
|
||||
--overflow-lines: 1;
|
||||
}
|
||||
.palette-colors {
|
||||
display: flex;
|
||||
|
|
@ -139,22 +138,37 @@
|
|||
.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 {
|
||||
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;
|
||||
}
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.edit-labels-pop-over .card-label .viewer p {
|
||||
margin: 0;
|
||||
|
|
@ -162,6 +176,34 @@
|
|||
.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;
|
||||
|
|
@ -170,6 +212,24 @@
|
|||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,10 @@
|
|||
.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;
|
||||
height: min-content;
|
||||
margin-bottom: 1.2vh;
|
||||
}
|
||||
.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;
|
||||
|
|
@ -50,25 +28,32 @@
|
|||
.minicard-wrapper .multi-selection-checkbox + .minicard {
|
||||
margin-left: 1vw;
|
||||
}
|
||||
.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;
|
||||
@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-actions-right {
|
||||
justify-content: end;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
gap: .5lh;
|
||||
.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;
|
||||
}
|
||||
@media print {
|
||||
.minicard-details-menu,
|
||||
|
|
@ -91,6 +76,7 @@
|
|||
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),
|
||||
|
|
@ -110,30 +96,20 @@
|
|||
margin: 0.8vh -1vw 0.8vh -1vw;
|
||||
border-radius: top 0.3vw;
|
||||
}
|
||||
.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-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-custom-fields {
|
||||
display: block;
|
||||
|
|
@ -145,22 +121,26 @@
|
|||
.minicard .minicard-custom-field-item {
|
||||
flex-grow: 1;
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
max-width: 13vw;
|
||||
margin-right: 0.5vw;
|
||||
}
|
||||
.minicard .minicard-custom-field-item-fullwidth {
|
||||
flex-grow: 1;
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
word-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;
|
||||
|
|
@ -174,34 +154,58 @@
|
|||
text-align: center;
|
||||
}
|
||||
.minicard .minicard-title {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
cursor: grab;
|
||||
.viewer {
|
||||
--overflow-lines: 2;
|
||||
}
|
||||
|
||||
margin-right: 1.5vw;
|
||||
}
|
||||
.minicard .minicard-title .card-number {
|
||||
color: #b3b3b3;
|
||||
display: inline-block;
|
||||
margin-right: 0.7vw;
|
||||
}
|
||||
.minicard .date {
|
||||
display: flex;
|
||||
&>a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
align-self: stretch;
|
||||
@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;
|
||||
}
|
||||
|
||||
/* 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: #d3d3d3; /* Grey for received - a bit darker than base card-date */
|
||||
background-color: #dbdbdb; /* Grey for received - same as base card-date */
|
||||
}
|
||||
|
||||
.minicard .card-date.received-date:hover,
|
||||
|
|
@ -307,134 +311,102 @@
|
|||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5ch;
|
||||
float: left;
|
||||
margin-top: 1vh;
|
||||
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.3lh 0.8ch;
|
||||
padding: 0 0.4vw;
|
||||
border-radius: 0.4vw;
|
||||
&, .fa {
|
||||
color: #fff;
|
||||
}
|
||||
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 {
|
||||
display: flex;
|
||||
float: right;
|
||||
border-radius: 50%;
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 0.2lh;
|
||||
height: clamp(24px, 3.5vw, 32px);
|
||||
width: clamp(24px, 3.5vw, 32px);
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
|
||||
.minicard .minicard-assignees .member {
|
||||
border: 2px solid rgb(180, 87, 87);
|
||||
.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-creator .member {
|
||||
border: 2px solid #7fd67f;
|
||||
}
|
||||
|
||||
.minicard .minicard-members .member {
|
||||
border: 2px solid #5a5ac6;
|
||||
.minicard .minicard-members + .badges,
|
||||
.minicard .minicard-assignees + .badges,
|
||||
.minicard .minicard-creator + .badges {
|
||||
margin-top: 0.7vh;
|
||||
}
|
||||
.minicard .minicard-assignees {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f00;
|
||||
}
|
||||
.minicard .minicard-creator {
|
||||
border-bottom: 1px solid #008000;
|
||||
}
|
||||
|
||||
.minicard .minicard-members:empty,
|
||||
.minicard .minicard-assignees:empty {
|
||||
display: none;
|
||||
}
|
||||
.minicard .minicard-description {
|
||||
padding: 0.8vh 0 0 1vw;
|
||||
color: #000;
|
||||
background-color: #eee;
|
||||
padding: 0.5lh 1ch;
|
||||
--overflow-lines: 2;
|
||||
.viewer {
|
||||
font-size: 0.9em;
|
||||
ul {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
gap: 0.3lh;
|
||||
padding: 0.5lh 1ch;
|
||||
position: relative;
|
||||
margin-bottom: 1.3vh;
|
||||
}
|
||||
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea,
|
||||
.minicard.minicard-composer textarea.minicard-composer-textarea:focus {
|
||||
resize: none;
|
||||
|
|
@ -443,11 +415,11 @@
|
|||
box-shadow: none;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 1ch;
|
||||
min-height: 5lh;
|
||||
padding: 0;
|
||||
max-height: 22vh;
|
||||
min-height: 5vh;
|
||||
margin-bottom: 2.5vh;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
.parent-prefix {
|
||||
color: #b3b3b3;
|
||||
|
|
@ -762,12 +734,30 @@
|
|||
|
||||
/* List name display on minicard */
|
||||
.minicard-list-name {
|
||||
font-size: inherit;
|
||||
font-size: 0.75em;
|
||||
color: #8c8c8c;
|
||||
margin-top: 0.2vh;
|
||||
display: flex;
|
||||
padding: 0 0.5ch;
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 0.5ch;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3vh;
|
||||
}
|
||||
|
||||
.minicard-checklist .checklist-title {
|
||||
|
|
|
|||
|
|
@ -1,236 +1,227 @@
|
|||
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 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 getReceived
|
||||
.date
|
||||
+minicardReceivedDate
|
||||
if getStart
|
||||
.date
|
||||
+minicardStartDate
|
||||
if getDue
|
||||
.date
|
||||
+minicardDueDate
|
||||
if getEnd
|
||||
+minicardEndDate
|
||||
if getSpentTime
|
||||
.date.viewer
|
||||
.date
|
||||
+cardSpentTime
|
||||
.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} 
|
||||
= 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}}');")
|
||||
if cover
|
||||
if currentBoard.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'}}
|
||||
|
||||
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-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}}")
|
||||
|
||||
.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
|
||||
.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"
|
||||
+viewer
|
||||
= 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)
|
||||
= 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
|
||||
|
||||
if allowsMembers
|
||||
if getMembers
|
||||
.minicard-people-wrapper
|
||||
.minicard-members.js-minicard-members
|
||||
each getMembers
|
||||
+userAvatar(userId=this)
|
||||
if showAssignee
|
||||
if getAssignees
|
||||
.minicard-assignees.js-minicard-assignees
|
||||
each getAssignees
|
||||
+userAvatar(userId=this)
|
||||
|
||||
.minicard-badges-and-creator
|
||||
if allowsCreatorOnMinicard
|
||||
.minicard-creator
|
||||
+userAvatar(userId=this.userId noRemove=true)
|
||||
if showMembers
|
||||
if getMembers
|
||||
.minicard-members.js-minicard-members
|
||||
each getMembers
|
||||
+userAvatar(userId=this)
|
||||
|
||||
.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 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
|
||||
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' value=sort dir="auto")
|
||||
input.js-edit-card-sort-popup(type='text' autofocus value=sort dir="auto")
|
||||
.edit-controls.clearfix
|
||||
button.primary.confirm.js-submit-edit-card-sort-popup(type="submit") {{_ 'save'}}
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,24 +13,6 @@ 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()
|
||||
|
|
@ -57,14 +39,46 @@ 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());
|
||||
},
|
||||
|
||||
isSelected() {
|
||||
const card = this.currentData();
|
||||
return Session.get('currentCard') === card._id;
|
||||
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;
|
||||
},
|
||||
|
||||
/** opens the card label popup only if clicked onto a label
|
||||
|
|
@ -73,8 +87,6 @@ BlazeComponent.extendComponent({
|
|||
*/
|
||||
cardLabelsPopup(event) {
|
||||
if (this.find('.js-card-label:hover')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
Popup.open("cardLabels")(event, {dataContextIfCurrentDataIsUndefined: this.currentData()});
|
||||
}
|
||||
},
|
||||
|
|
@ -242,8 +254,33 @@ Template.minicard.helpers({
|
|||
},
|
||||
|
||||
shouldShowListOnMinicard() {
|
||||
return Utils.allowsShowLists();
|
||||
// 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;
|
||||
},
|
||||
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
.result-card-context-list {
|
||||
display: flex;
|
||||
gap: 0.2ch;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
.result-card-block-wrapper {
|
||||
display: inline-block;
|
||||
|
|
|
|||
|
|
@ -14,10 +14,17 @@ 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 [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,14 +4,19 @@
|
|||
textarea.js-add-subtask-item,
|
||||
textarea.js-edit-subtask-item {
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
word-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,
|
||||
|
|
@ -23,11 +28,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 {
|
||||
|
|
@ -128,7 +133,7 @@ textarea.js-edit-subtask-item {
|
|||
margin: 0.1em 0 0 0;
|
||||
}
|
||||
.subtasks-item .check-box.is-checked {
|
||||
border-bottom: 0.2ch solid #3cb500;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
|
|
@ -160,4 +165,16 @@ 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,12 @@ template(name="subtasks")
|
|||
| {{_ 'subtasks'}}
|
||||
if currentUser.isBoardAdmin
|
||||
if toggleDeleteDialog.get
|
||||
.board-overlay#card-details-overlay
|
||||
+subtaskDeleteDialog(subtask = subtaskToDelete)
|
||||
|
||||
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
|
||||
| ❌
|
||||
|
||||
.card-subtasks-items
|
||||
each subtask in currentCard.subtasks
|
||||
+subtaskDetail(subtask = subtask)
|
||||
|
||||
if canModifyCard
|
||||
+inlinedForm(autoclose=false classNames="js-add-subtask" cardId = cardId)
|
||||
|
|
@ -32,6 +24,9 @@ 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
|
||||
|
|
@ -42,13 +37,13 @@ template(name="subtaskDetail")
|
|||
= subtask.title
|
||||
|
||||
template(name="addSubtaskItemForm")
|
||||
textarea.js-add-subtask-item(rows='1' dir="auto")
|
||||
textarea.js-add-subtask-item(rows='1' autofocus 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' dir="auto")
|
||||
textarea.js-edit-subtask-item(rows='1' autofocus dir="auto")
|
||||
if $eq type 'item'
|
||||
= item.title
|
||||
else
|
||||
|
|
@ -57,6 +52,9 @@ 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
|
||||
|
|
@ -102,3 +100,4 @@ template(name="subtaskActionsPopup")
|
|||
a.js-delete-subtask.delete-subtask
|
||||
i.fa.fa-trash
|
||||
| {{_ "delete"}} ...
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
.original-position-info {
|
||||
margin: 5px 0;
|
||||
padding: 8px;
|
||||
border-radius: 0.4ch;
|
||||
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
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,7 +78,7 @@
|
|||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.original-position-info {
|
||||
|
||||
font-size: 11px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<span class="original-position-text">✅ In original position</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{#if getOriginalTitle}}
|
||||
<div class="original-title">
|
||||
<strong>Original title:</strong> {{getOriginalTitle}}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
this.originalPosition = new ReactiveVar(null);
|
||||
this.isLoading = new ReactiveVar(false);
|
||||
this.hasMoved = new ReactiveVar(false);
|
||||
|
||||
|
||||
this.autorun(() => {
|
||||
const data = this.data();
|
||||
if (data && data.entityId && data.entityType) {
|
||||
|
|
@ -24,9 +24,9 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
|
||||
loadOriginalPosition(entityId, entityType) {
|
||||
this.isLoading.set(true);
|
||||
|
||||
|
||||
const methodName = `positionHistory.get${entityType.charAt(0).toUpperCase() + entityType.slice(1)}OriginalPosition`;
|
||||
|
||||
|
||||
Meteor.call(methodName, entityId, (error, result) => {
|
||||
this.isLoading.set(false);
|
||||
if (error) {
|
||||
|
|
@ -34,7 +34,7 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
this.originalPosition.set(null);
|
||||
} else {
|
||||
this.originalPosition.set(result);
|
||||
|
||||
|
||||
// Check if the entity has moved
|
||||
const movedMethodName = `positionHistory.has${entityType.charAt(0).toUpperCase() + entityType.slice(1)}Moved`;
|
||||
Meteor.call(movedMethodName, entityId, (movedError, movedResult) => {
|
||||
|
|
@ -61,11 +61,11 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
getOriginalPositionDescription() {
|
||||
const position = this.getOriginalPosition();
|
||||
if (!position) return 'No original position data';
|
||||
|
||||
|
||||
if (position.originalPosition) {
|
||||
const entityType = this.data().entityType;
|
||||
let description = `Original position: ${position.originalPosition.sort || 0}`;
|
||||
|
||||
|
||||
if (entityType === 'list' && position.originalSwimlaneId) {
|
||||
description += ` in swimlane ${position.originalSwimlaneId}`;
|
||||
} else if (entityType === 'card') {
|
||||
|
|
@ -76,10 +76,10 @@ class OriginalPositionComponent extends BlazeComponent {
|
|||
description += ` in list ${position.originalListId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
|
||||
return 'No original position data';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,3 @@
|
|||
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]),
|
||||
|
|
@ -20,8 +7,9 @@ button {
|
|||
border: 1px solid #ccc;
|
||||
border-radius: 0.4vw;
|
||||
display: block;
|
||||
padding: 0.3lh 1ch;
|
||||
max-width: clamp(30vw, 100%, 800px);
|
||||
margin-bottom: 1.5vh;
|
||||
min-height: 4.5vh;
|
||||
padding: 1vh 1vw;
|
||||
}
|
||||
select.full,
|
||||
textarea.full,
|
||||
|
|
@ -54,6 +42,18 @@ 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,6 +102,11 @@ textarea:disabled {
|
|||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
select {
|
||||
max-height: 40vh;
|
||||
width: min(256px, 32vw);
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
select.inline {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -109,11 +114,14 @@ option[disabled] {
|
|||
color: #222;
|
||||
}
|
||||
textarea {
|
||||
height: 20vh;
|
||||
transition: background 85ms ease-in, border-color 85ms ease-in;
|
||||
resize: vertical;
|
||||
width: auto;
|
||||
font-size: 0.9em;
|
||||
min-height: 3lh;
|
||||
width: 100%;
|
||||
}
|
||||
textarea.editor {
|
||||
resize: none;
|
||||
padding-bottom: 3vh;
|
||||
}
|
||||
.button {
|
||||
border-radius: 3px;
|
||||
|
|
@ -129,16 +137,9 @@ button {
|
|||
display: inline-block;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
/* 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;
|
||||
padding: 1vh 2.5vw;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
z-index: 1;
|
||||
:not(.password-toggle-btn) {
|
||||
margin-top: 0.1lh;
|
||||
}
|
||||
}
|
||||
input[type="submit"] .wide,
|
||||
button .wide {
|
||||
|
|
@ -240,9 +241,9 @@ input[type="hidden"] {
|
|||
}
|
||||
.radio-div,
|
||||
.check-div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2lh;
|
||||
display: block;
|
||||
margin: 0 0 0.5vh 2.5vw;
|
||||
min-height: 2.5vh;
|
||||
position: relative;
|
||||
}
|
||||
.radio-div input,
|
||||
|
|
@ -259,10 +260,9 @@ input[type="hidden"] {
|
|||
font-weight: 400;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5vh;
|
||||
}
|
||||
label.form-error {
|
||||
color: #d32f2f;
|
||||
|
|
@ -274,32 +274,11 @@ textarea::-moz-placeholder {
|
|||
color: #333 !important;
|
||||
}
|
||||
.edit-controls,
|
||||
.add-controls,
|
||||
.links-controls {
|
||||
.add-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 1.5vh;
|
||||
}
|
||||
@media print {
|
||||
.add-controls {
|
||||
|
|
@ -310,7 +289,14 @@ textarea::-moz-placeholder {
|
|||
.add-controls button[type=submit],
|
||||
.edit-controls input[type=button],
|
||||
.add-controls input[type=button] {
|
||||
margin: 0;
|
||||
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;
|
||||
}
|
||||
[type="checkbox"]:not(:checked),
|
||||
[type="checkbox"]:checked {
|
||||
|
|
@ -320,18 +306,6 @@ 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;
|
||||
|
|
@ -343,33 +317,19 @@ textarea::-moz-placeholder {
|
|||
cursor: pointer;
|
||||
}
|
||||
.materialCheckBox.is-checked {
|
||||
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;
|
||||
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);
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
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;
|
||||
}
|
||||
transform-origin: 100% 100%;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .materialCheckBox.is-checked {
|
||||
|
|
@ -402,7 +362,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;
|
||||
|
|
@ -459,7 +419,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;
|
||||
}
|
||||
|
|
@ -468,7 +428,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;
|
||||
|
|
@ -612,7 +572,7 @@ button.loud-text-button:hover {
|
|||
padding: 11px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.big-link .text {
|
||||
|
|
@ -655,7 +615,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;
|
||||
|
|
@ -695,7 +655,7 @@ button.loud-text-button:hover {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
font-size: 23px;
|
||||
}
|
||||
.uploader .realfile input[type="file"] {
|
||||
cursor: pointer;
|
||||
|
|
@ -706,7 +666,7 @@ button.loud-text-button:hover {
|
|||
padding: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
font-size: 23px;
|
||||
}
|
||||
.uploader:hover .fakefile {
|
||||
background: #318ec4;
|
||||
|
|
@ -745,13 +705,13 @@ button.loud-text-button:hover {
|
|||
color: #fff;
|
||||
}
|
||||
.material-toggle-switch {
|
||||
padding: 0.2rlh 1ch;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
}
|
||||
.toggle-label {
|
||||
height: 0.6rlh;
|
||||
width: 1.3rlh;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 20px;
|
||||
width: 44px;
|
||||
background-color: #a6a6a6;
|
||||
border-radius: 100px;
|
||||
cursor: pointer;
|
||||
|
|
@ -759,13 +719,11 @@ button.loud-text-button:hover {
|
|||
}
|
||||
.toggle-label:after {
|
||||
position: absolute;
|
||||
/* ensure vertical centering */
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -0.2rlh;
|
||||
width: .8rlh;
|
||||
height: .8rlh;
|
||||
left: -2px;
|
||||
top: -3px;
|
||||
display: block;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 100px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 3px 3px rgba(0,0,0,0.05);
|
||||
|
|
@ -779,7 +737,7 @@ button.loud-text-button:hover {
|
|||
background-color: #6fbeb5;
|
||||
}
|
||||
.toggle-switch:checked ~ .toggle-label:after {
|
||||
left: 1.5ch;
|
||||
left: 20px;
|
||||
background-color: #179588;
|
||||
}
|
||||
.toggle-switch:checked:disabled ~ .toggle-label {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ BlazeComponent.extendComponent({
|
|||
const results = UserSearchIndex.search(query, { limit: 20 }).fetch();
|
||||
this.searchResults.set(results);
|
||||
this.searching.set(false);
|
||||
|
||||
|
||||
if (results.length === 0) {
|
||||
this.noResults.set(true);
|
||||
}
|
||||
|
|
@ -358,11 +358,11 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
'keyup .js-search-member-input'(event) {
|
||||
const query = event.target.value.trim();
|
||||
|
||||
|
||||
if (this.searchTimeout) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
|
||||
|
||||
this.searchTimeout = setTimeout(() => {
|
||||
this.performSearch(query);
|
||||
}, 300);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,12 @@
|
|||
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
|
||||
|
|
@ -4,8 +4,6 @@ require('/client/lib/jquery-ui.js')
|
|||
|
||||
const { calculateIndex } = Utils;
|
||||
|
||||
export const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// Proxy
|
||||
openForm(options) {
|
||||
|
|
@ -14,7 +12,6 @@ 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
|
||||
|
|
@ -25,32 +22,178 @@ BlazeComponent.extendComponent({
|
|||
// callback, we basically solve all issues related to reactive updates. A
|
||||
// comment below provides further details.
|
||||
onRendered() {
|
||||
this.list = this.firstNode();
|
||||
this.resizeHandle = this.find('.js-list-resize-handle');
|
||||
const boardComponent = this.parentComponent().parentComponent();
|
||||
|
||||
// Initialize list resize functionality immediately
|
||||
this.initializeListResize();
|
||||
|
||||
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();
|
||||
}
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
// Reactively update collapse appearance and resize handle visibility when auto-width or collapse changes
|
||||
this.autorun(() => {
|
||||
ensureCollapseState(Utils.getListCollapseState(this.data()));
|
||||
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(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
collapsed() {
|
||||
return this.collapse.get();
|
||||
},
|
||||
|
||||
|
||||
listWidth() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
|
|
@ -79,7 +222,7 @@ BlazeComponent.extendComponent({
|
|||
listConstraint() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const list = Template.currentData();
|
||||
if (!list) return 0;
|
||||
if (!list) return 550; // Return default constraint if list is not available
|
||||
|
||||
if (user) {
|
||||
// For logged-in users, get from user profile
|
||||
|
|
@ -97,7 +240,7 @@ BlazeComponent.extendComponent({
|
|||
} catch (e) {
|
||||
console.warn('Error reading list constraint from localStorage:', e);
|
||||
}
|
||||
return 0;
|
||||
return 550; // Return default constraint if not found
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -113,14 +256,18 @@ BlazeComponent.extendComponent({
|
|||
|
||||
initializeListResize() {
|
||||
// Check if we're still in a valid template context
|
||||
if (!this.data()) {
|
||||
if (!Template.currentData()) {
|
||||
console.warn('No current template data available for list resize initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
const list = Template.currentData();
|
||||
const $list = this.$('.js-list');
|
||||
const $resizeHandle = this.$('.js-list-resize-handle');
|
||||
|
||||
// Check if elements exist
|
||||
if (!this.list || !this.resizeHandle) {
|
||||
console.info('List or resize handle not found, retrying in 100ms');
|
||||
if (!$list.length || !$resizeHandle.length) {
|
||||
console.warn('List or resize handle not found, retrying in 100ms');
|
||||
Meteor.setTimeout(() => {
|
||||
if (!this.isDestroyed) {
|
||||
this.initializeListResize();
|
||||
|
|
@ -129,117 +276,95 @@ BlazeComponent.extendComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
let isResizing = false;
|
||||
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) => {
|
||||
// gain access to modern attributes e.g. isPrimary
|
||||
e = e.originalEvent;
|
||||
|
||||
if (isResizing || Utils.shouldIgnorePointer(e)) {
|
||||
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
|
||||
|
||||
const startResize = (e) => {
|
||||
isResizing = true;
|
||||
startX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
startWidth = $list.outerWidth();
|
||||
|
||||
|
||||
// Add visual feedback
|
||||
$list.addClass('list-resizing');
|
||||
$('body').addClass('list-resizing-active');
|
||||
|
||||
|
||||
// Prevent text selection during resize
|
||||
$('body').css('user-select', 'none');
|
||||
|
||||
e.preventDefault();
|
||||
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) => {
|
||||
e = e.originalEvent;
|
||||
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.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) => {
|
||||
e = e.originalEvent;
|
||||
if (!isResizing) return;
|
||||
|
||||
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;
|
||||
|
||||
if (previousLimit) {
|
||||
component.list.classList.remove('cannot-resize');
|
||||
}
|
||||
// Calculate final width
|
||||
const currentX = e.pageX || e.originalEvent.touches[0].pageX;
|
||||
const deltaX = currentX - startX;
|
||||
const finalWidth = Math.max(minWidth, startWidth + deltaX);
|
||||
|
||||
const finalWidth = parseInt(component.list.style.getPropertyValue('--list-width'), 10);
|
||||
// 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 height
|
||||
component.list.classList.remove('list-resizing');
|
||||
document.body.classList.remove('list-resizing-active');
|
||||
// Remove visual feedback but keep the width
|
||||
$list.removeClass('list-resizing');
|
||||
$('body').removeClass('list-resizing-active');
|
||||
$('body').css('user-select', '');
|
||||
|
||||
if (component.collapse.get()) {
|
||||
return;
|
||||
}
|
||||
// Keep the CSS custom property for persistent width
|
||||
// The CSS custom property will remain on the element to maintain the width
|
||||
|
||||
// Save the new width using the existing system
|
||||
const list = component.data();
|
||||
const boardId = list.boardId;
|
||||
const listId = list._id;
|
||||
|
||||
|
|
@ -250,7 +375,7 @@ BlazeComponent.extendComponent({
|
|||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
// For logged-in users, use server method
|
||||
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, maxWidth, (error, result) => {
|
||||
Meteor.call('applyListWidthToStorage', boardId, listId, finalWidth, listConstraint, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error saving list width:', error);
|
||||
} else {
|
||||
|
|
@ -293,8 +418,32 @@ BlazeComponent.extendComponent({
|
|||
e.preventDefault();
|
||||
};
|
||||
|
||||
// handle both pointer and touch
|
||||
$(this.resizeHandle).on("pointerdown", startResize);
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up on component destruction
|
||||
component.onDestroyed(() => {
|
||||
|
|
@ -306,6 +455,12 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('list');
|
||||
|
||||
Template.list.helpers({
|
||||
collapsed() {
|
||||
return Utils.getListCollapseState(this);
|
||||
},
|
||||
});
|
||||
|
||||
Template.miniList.events({
|
||||
'click .js-select-list'() {
|
||||
const listId = this._id;
|
||||
|
|
@ -313,10 +468,15 @@ Template.miniList.events({
|
|||
},
|
||||
});
|
||||
|
||||
Template.miniList.helpers({
|
||||
isCurrentList() {
|
||||
const currentList = Utils.getCurrentList();
|
||||
const list = Template.currentData();
|
||||
return currentList && currentList._id == list._id;
|
||||
},
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,18 +4,17 @@ template(name="listBody")
|
|||
.minicards.clearfix.js-minicards(class="{{#if reachedWipLimit}}js-list-full{{/if}}")
|
||||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
if customFieldSum.lenght
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
ul.sidebar-list
|
||||
each customFieldsSum
|
||||
li
|
||||
+viewer
|
||||
= name
|
||||
if $eq customFieldsSum.type "number"
|
||||
+viewer
|
||||
= name
|
||||
if $eq customFieldsSum.type "number"
|
||||
+viewer
|
||||
= value
|
||||
if $eq customFieldsSum.type "currency"
|
||||
+viewer
|
||||
= formattedCurrencyCustomFieldValue(value)
|
||||
= 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}}"
|
||||
|
|
@ -26,15 +25,15 @@ template(name="listBody")
|
|||
+minicard(this)
|
||||
if (showSpinner (idOrNull ../../_id))
|
||||
+spinnerList
|
||||
|
||||
if canSeeAddCard
|
||||
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
|
||||
+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")
|
||||
|
||||
template(name="spinnerList")
|
||||
.sk-spinner.sk-spinner-list(
|
||||
|
|
@ -44,30 +43,33 @@ 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")
|
||||
.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
|
||||
if members.get
|
||||
.minicard-members.js-minicard-composer-members
|
||||
each members.get
|
||||
+userAvatar(userId=this)
|
||||
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
|
||||
.links-controls.clearfix
|
||||
.add-controls.clearfix
|
||||
button.primary.confirm(type="submit") {{_ 'add'}}
|
||||
a.js-close-inlined-form
|
||||
i.fa.fa-times-thin
|
||||
.add-controls.clearfix
|
||||
unless currentBoard.isTemplatesBoard
|
||||
unless currentBoard.isTemplateBoard
|
||||
span.quiet
|
||||
| {{_ 'or'}}
|
||||
a.js-link {{_ 'link'}}
|
||||
span.quiet
|
||||
|
|
||||
| /
|
||||
a.js-search {{_ 'search'}}
|
||||
span.quiet
|
||||
|
|
||||
| /
|
||||
a.js-card-template {{_ 'template'}}
|
||||
|
||||
template(name="autocompleteLabelLine")
|
||||
|
|
@ -75,73 +77,70 @@ template(name="autocompleteLabelLine")
|
|||
span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
|
||||
|
||||
template(name="linkCardPopup")
|
||||
label {{_ 'boards'}}:
|
||||
.link-board-wrapper
|
||||
.link-board-dropdown
|
||||
label {{_ 'boards'}}:
|
||||
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
|
||||
select.js-select-boards
|
||||
option(value="")
|
||||
each boards
|
||||
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)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -3,168 +3,16 @@ 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 [];
|
||||
},
|
||||
|
|
@ -234,10 +82,9 @@ BlazeComponent.extendComponent({
|
|||
evt.preventDefault();
|
||||
const firstCardDom = this.find('.js-minicard:first');
|
||||
const lastCardDom = this.find('.js-minicard:last');
|
||||
// more robust to start from the form
|
||||
const textarea = $(evt.currentTarget).closest('.inlined-form').find('textarea');
|
||||
const textarea = $(evt.currentTarget).find('textarea');
|
||||
const position = this.currentData().position;
|
||||
const title = $(textarea).val().trim();
|
||||
const title = textarea.val().trim();
|
||||
|
||||
let sortIndex;
|
||||
if (position === 'top') {
|
||||
|
|
@ -321,6 +168,7 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// We keep the form opened, empty it, and scroll to it.
|
||||
textarea.val('').focus();
|
||||
autosize.update(textarea);
|
||||
if (position === 'bottom') {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
|
@ -346,19 +194,21 @@ 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)) {
|
||||
// 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);
|
||||
evt.stopImmediatePropagation();
|
||||
evt.preventDefault();
|
||||
Utils.goBoardId(Session.get('currentBoard'));
|
||||
} else {
|
||||
// Allow normal href navigation, but if it's the same card URL,
|
||||
|
|
@ -433,6 +283,12 @@ BlazeComponent.extendComponent({
|
|||
return user && user.isVerticalScrollbars();
|
||||
},
|
||||
|
||||
cardDetailsPopup(event) {
|
||||
if (!Popup.isOpen()) {
|
||||
Popup.open("cardDetails")(event);
|
||||
}
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -440,8 +296,6 @@ 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
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
@ -547,17 +401,6 @@ 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();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
|
@ -566,6 +409,8 @@ BlazeComponent.extendComponent({
|
|||
const editor = this;
|
||||
const $textarea = this.$('textarea');
|
||||
|
||||
autosize($textarea);
|
||||
|
||||
$textarea.escapeableTextComplete(
|
||||
[
|
||||
// User mentions
|
||||
|
|
@ -576,9 +421,7 @@ BlazeComponent.extendComponent({
|
|||
callback(
|
||||
$.map(currentBoard.activeMembers(), member => {
|
||||
const user = ReactiveCache.getUser(member.userId);
|
||||
return user.username.indexOf(term) === 0 &&
|
||||
// don't show already selected members
|
||||
!editor.members.get().find((e) => e === member.userId) ? user : null;
|
||||
return user.username.indexOf(term) === 0 ? user : null;
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
@ -602,12 +445,8 @@ BlazeComponent.extendComponent({
|
|||
const currentBoard = Utils.getCurrentBoard();
|
||||
callback(
|
||||
$.map(currentBoard.labels, label => {
|
||||
if (
|
||||
label.name == undefined ||
|
||||
// don't show already selected labels
|
||||
editor.getLabels().find((e) => e._id === label._id)
|
||||
) {
|
||||
return null;
|
||||
if (label.name == undefined) {
|
||||
label.name = "";
|
||||
}
|
||||
if (
|
||||
label.name.indexOf(term) > -1 ||
|
||||
|
|
@ -664,10 +503,10 @@ BlazeComponent.extendComponent({
|
|||
subManager.subscribe('board', this.boardId, false);
|
||||
this.board = ReactiveCache.getBoard(this.boardId);
|
||||
// List where to insert card
|
||||
this.list = $(PopupComponent.stack[0].openerElement).closest('.js-list');
|
||||
this.list = $(Popup._getTopStack().openerElement).closest('.js-list');
|
||||
this.listId = Blaze.getData(this.list[0])._id;
|
||||
// Swimlane where to insert card
|
||||
const swimlane = $(PopupComponent.stack[0].openerElement).closest(
|
||||
const swimlane = $(Popup._getTopStack().openerElement).closest(
|
||||
'.js-swimlane',
|
||||
);
|
||||
this.swimlaneId = '';
|
||||
|
|
@ -720,8 +559,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
const lists = ReactiveCache.getLists(
|
||||
{
|
||||
boardId: this.selectedBoardId.get(),
|
||||
swimlaneId: this.selectedSwimlaneId?.get?.()
|
||||
boardId: this.selectedBoardId.get()
|
||||
},
|
||||
{
|
||||
sort: { sort: 1 },
|
||||
|
|
@ -865,16 +703,16 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
onCreated() {
|
||||
this.isCardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
|
||||
this.isCardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
|
||||
'js-card-template',
|
||||
);
|
||||
this.isListTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
|
||||
this.isListTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
|
||||
'js-list-template',
|
||||
);
|
||||
this.isSwimlaneTemplateSearch = $(
|
||||
PopupComponent.stack[0].openerElement,
|
||||
Popup._getTopStack().openerElement,
|
||||
).hasClass('js-open-add-swimlane-menu');
|
||||
this.isBoardTemplateSearch = $(PopupComponent.stack[0].openerElement).hasClass(
|
||||
this.isBoardTemplateSearch = $(Popup._getTopStack().openerElement).hasClass(
|
||||
'js-add-board',
|
||||
);
|
||||
this.isTemplateSearch =
|
||||
|
|
@ -893,16 +731,20 @@ BlazeComponent.extendComponent({
|
|||
} else {
|
||||
this.board = Utils.getCurrentBoard();
|
||||
}
|
||||
this.boardId = this.board?._id;
|
||||
if (!this.board) {
|
||||
Popup.back();
|
||||
return;
|
||||
}
|
||||
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 = $(PopupComponent.stack[0].openerElement).parents(
|
||||
const swimlane = $(Popup._getTopStack().openerElement).parents(
|
||||
'.js-swimlane',
|
||||
);
|
||||
if (Utils.boardView() === 'board-view-swimlanes')
|
||||
|
|
@ -941,7 +783,11 @@ BlazeComponent.extendComponent({
|
|||
} else if (this.isSwimlaneTemplateSearch) {
|
||||
return board.searchSwimlanes(this.term.get());
|
||||
} else if (this.isBoardTemplateSearch) {
|
||||
return board.searchBoards(this.term.get());
|
||||
const boards = board.searchBoards(this.term.get());
|
||||
boards.forEach(board => {
|
||||
subManager.subscribe('board', board.linkedId, false);
|
||||
});
|
||||
return boards;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,68 +9,66 @@ template(name="listHeader")
|
|||
if currentList
|
||||
a.list-header-left-icon.js-unselect-list
|
||||
i.fa.fa-caret-left
|
||||
else
|
||||
//- start by this on mobile to have cohesion with other views
|
||||
a.list-header-menu-icon.js-select-list
|
||||
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
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
else
|
||||
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
|
||||
if collapsed
|
||||
i.fa.fa-caret-right
|
||||
.list-header-name-container
|
||||
else
|
||||
i.fa.fa-caret-down
|
||||
div(class="{{#if collapsed}}list-rotated{{/if}}")
|
||||
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}}")
|
||||
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
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
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
unless collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
if hasNumberFieldsSum
|
||||
|
|
||||
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 isMiniScreen
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle.handle.js-list-handle
|
||||
i.fa.fa-arrows
|
||||
else if currentUser.isBoardMember
|
||||
|
|
@ -79,13 +77,25 @@ template(name="listHeader")
|
|||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
if isMiniScreen
|
||||
if isTouchScreenOrShowDesktopDragHandles
|
||||
a.list-header-handle-desktop.handle.js-list-handle(title="{{_ 'drag-list'}}")
|
||||
i.fa.fa-arrows
|
||||
unless isMiniScreen
|
||||
if collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}}
|
||||
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
|
||||
|
||||
template(name="editListTitleForm")
|
||||
.list-composer
|
||||
|
|
@ -185,8 +195,10 @@ template(name="listMorePopup")
|
|||
| {{_ 'added'}}
|
||||
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
|
||||
//unless currentUser.isWorker
|
||||
// if currentUser.isBoardAdmin
|
||||
// a.js-delete {{_ 'delete'}}
|
||||
//
|
||||
if currentUser.isBoardAdmin
|
||||
//
|
||||
a.js-delete {{_ 'delete'}}
|
||||
|
||||
template(name="listDeletePopup")
|
||||
p {{_ "list-delete-pop"}}
|
||||
|
|
@ -215,14 +227,14 @@ template(name="wipLimitErrorPopup")
|
|||
.wip-limit-invalid
|
||||
p {{_ 'wipLimitErrorPopup-dialog-pt1'}}
|
||||
p {{_ 'wipLimitErrorPopup-dialog-pt2'}}
|
||||
button.negate.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
button.full.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="100")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="100")
|
||||
input.list-width-value(type="number" value="{{ listWidthValue }}" min="270")
|
||||
input.list-constraint-value(type="number" value="{{ listConstraintValue }}" min="270")
|
||||
input.list-width-apply(type="submit" value="{{_ 'apply'}}")
|
||||
input.list-width-error
|
||||
br
|
||||
|
|
@ -233,8 +245,8 @@ template(name="setListWidthPopup")
|
|||
|
||||
template(name="listWidthErrorPopup")
|
||||
.list-width-invalid
|
||||
p {{_ 'list-width-error-message'}} '>=100'
|
||||
button.negate.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
p {{_ 'list-width-error-message'}} '>=270'
|
||||
button.full.js-back-view(type="submit") {{_ 'cancel'}}
|
||||
|
||||
template(name="setListColorPopup")
|
||||
form.edit-label
|
||||
|
|
|
|||
|
|
@ -9,15 +9,6 @@ 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 (
|
||||
|
|
@ -43,7 +34,7 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
collapsed(check = undefined) {
|
||||
const list = this.data();
|
||||
const list = Template.currentData();
|
||||
const status = Utils.getListCollapseState(list);
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
|
|
@ -119,11 +110,7 @@ BlazeComponent.extendComponent({
|
|||
return TAPi18n.__('cards-count');
|
||||
}
|
||||
},
|
||||
currentList() {
|
||||
const currentList = Utils.getCurrentList();
|
||||
const list = Template.currentData();
|
||||
return currentList && currentList._id == list._id;
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
|
|
@ -131,6 +118,10 @@ 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(
|
||||
|
|
@ -515,7 +506,7 @@ BlazeComponent.extendComponent({
|
|||
|
||||
let sortIndex = 0;
|
||||
const boardId = Utils.getCurrentBoardId();
|
||||
const swimlaneId = this.currentSwimlane?._id;
|
||||
let swimlaneId = this.currentSwimlane?._id;
|
||||
|
||||
const positionInput = this.find('.list-position-input');
|
||||
|
||||
|
|
@ -525,6 +516,9 @@ BlazeComponent.extendComponent({
|
|||
|
||||
if (selectedList) {
|
||||
sortIndex = selectedList.sort + 1;
|
||||
// Use the swimlane ID from the selected list to ensure the new list
|
||||
// is added to the same swimlane as the selected list
|
||||
swimlaneId = selectedList.swimlaneId;
|
||||
} else {
|
||||
// No specific position, add at end of swimlane
|
||||
if (swimlaneId) {
|
||||
|
|
@ -563,3 +557,4 @@ BlazeComponent.extendComponent({
|
|||
];
|
||||
},
|
||||
}).register('addListPopup');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.my-cards-board-wrapper {
|
||||
border-radius: 0 0 0.5vw 0.5vw;
|
||||
min-width: min(100%, 400px, 52vw);
|
||||
min-width: min(400px, 52vw);
|
||||
margin-bottom: 2.5vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
|
|
@ -33,6 +33,13 @@
|
|||
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;
|
||||
|
|
@ -74,7 +81,7 @@
|
|||
}
|
||||
|
||||
.accessibility-page h2 {
|
||||
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,14 +92,14 @@ BlazeComponent.extendComponent({
|
|||
class DueCardsComponent extends BlazeComponent {
|
||||
onCreated() {
|
||||
super.onCreated();
|
||||
|
||||
|
||||
this._cachedCards = null;
|
||||
this._cachedTimestamp = null;
|
||||
this.subscriptionHandle = null;
|
||||
this.isLoading = new ReactiveVar(true);
|
||||
this.hasResults = new ReactiveVar(false);
|
||||
this.searching = new ReactiveVar(false);
|
||||
|
||||
|
||||
// Subscribe to the optimized due cards publication
|
||||
this.autorun(() => {
|
||||
const allUsers = this.dueCardsView() === 'all';
|
||||
|
|
@ -107,7 +107,7 @@ class DueCardsComponent extends BlazeComponent {
|
|||
this.subscriptionHandle.stop();
|
||||
}
|
||||
this.subscriptionHandle = Meteor.subscribe('dueCards', allUsers);
|
||||
|
||||
|
||||
// Update loading state based on subscription
|
||||
this.autorun(() => {
|
||||
if (this.subscriptionHandle && this.subscriptionHandle.ready()) {
|
||||
|
|
@ -162,7 +162,7 @@ class DueCardsComponent extends BlazeComponent {
|
|||
// Get the translated text and manually replace %s with the count
|
||||
const baseText = TAPi18n.__('n-cards-found');
|
||||
const result = baseText.replace('%s', count);
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards: base text:', baseText, 'count:', count, 'result:', result);
|
||||
}
|
||||
|
|
@ -196,10 +196,10 @@ class DueCardsComponent extends BlazeComponent {
|
|||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards client: found', cards.length, 'cards with due dates');
|
||||
console.log('dueCards client: cards details:', cards.map(c => ({
|
||||
id: c._id,
|
||||
title: c.title,
|
||||
dueAt: c.dueAt,
|
||||
console.log('dueCards client: cards details:', cards.map(c => ({
|
||||
id: c._id,
|
||||
title: c.title,
|
||||
dueAt: c.dueAt,
|
||||
boardId: c.boardId,
|
||||
members: c.members,
|
||||
assignees: c.assignees,
|
||||
|
|
@ -223,11 +223,11 @@ class DueCardsComponent extends BlazeComponent {
|
|||
const isAssignee = card.assignees && card.assignees.includes(currentUser._id);
|
||||
const isAuthor = card.userId === currentUser._id;
|
||||
const matches = isMember || isAssignee || isAuthor;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true' && matches) {
|
||||
console.log('dueCards client: card matches user:', card.title, { isMember, isAssignee, isAuthor });
|
||||
}
|
||||
|
||||
|
||||
return matches;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
.new-comment, .inlined-form {
|
||||
a.fa.fa-brands.fa-markdown, a.fa.fa-copy {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.new-comment a.fa.fa-brands.fa-markdown,
|
||||
.inlined-form a.fa.fa-brands.fa-markdown {
|
||||
float: right;
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 60px;
|
||||
}
|
||||
.editor-controls {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
grid-area: editor-controls;
|
||||
align-items: center;
|
||||
align-self: start;
|
||||
gap: 1ch;
|
||||
.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 {
|
||||
grid-area: editor;
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
template(name="editor")
|
||||
.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'}}
|
||||
a.fa.fa-brands.fa-markdown(title="{{_ 'convert-to-markdown'}}")
|
||||
a.fa.fa-copy(title="{{_ 'copy-text-to-clipboard'}}")
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
textarea.editor(
|
||||
dir="auto"
|
||||
class="{{class}}"
|
||||
id=id
|
||||
autofocus=autofocus
|
||||
placeholder="{{_ 'comment-placeholder'}}")
|
||||
+Template.contentBlock
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ 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') {
|
||||
|
|
@ -411,14 +412,14 @@ Blaze.Template.registerHelper(
|
|||
if (knowedUser.userId === Meteor.userId()) {
|
||||
linkClass += ' me';
|
||||
}
|
||||
|
||||
|
||||
// For special group mentions, display translated text
|
||||
let displayText = knowedUser.username;
|
||||
if (specialHandleNames.includes(knowedUser.username)) {
|
||||
displayText = TAPi18n.__(knowedUser.username);
|
||||
linkClass = 'atMention'; // Remove js-open-member for special handles
|
||||
}
|
||||
|
||||
|
||||
// This @user mention link generation did open same Wekan
|
||||
// window in new tab, so now A is changed to U so it's
|
||||
// underlined and there is no link popup. This way also
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.global-search-board-wrapper {
|
||||
border-radius: 0.8ch;
|
||||
min-width: min(100%, 400px);
|
||||
border-radius: 8px;
|
||||
min-width: 400px;
|
||||
border-width: 8px;
|
||||
border-color: #808080;
|
||||
border-style: solid;
|
||||
|
|
@ -67,6 +67,8 @@
|
|||
color: #8b0000;
|
||||
}
|
||||
.global-search-page {
|
||||
width: 40%;
|
||||
min-width: 400px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
line-height: 150%;
|
||||
|
|
@ -89,13 +91,6 @@
|
|||
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
|
|
@ -5,81 +5,106 @@ template(name="header")
|
|||
Reddit "subreddit" bar.
|
||||
The first link goes to the boards page.
|
||||
if currentUser
|
||||
#header-quick-access(class="currentBoard.colorClass {{#if isMiniScreen}}mobile-view{{/if}}")
|
||||
#header-quick-access(class=currentBoard.colorClass)
|
||||
// Home icon - always at left side of logo
|
||||
#header-quick-access-left
|
||||
span.home-icon.allBoards
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.emoji-icon
|
||||
i.fa.fa-home
|
||||
span
|
||||
| {{_ 'all-boards'}}
|
||||
span.home-icon.allBoards
|
||||
a(href="{{pathFor 'home'}}")
|
||||
i.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
|
||||
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'}}
|
||||
// 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
|
||||
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
|
||||
// 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
|
||||
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
|
||||
div#headerIsSettingDatabaseCallDone.logo
|
||||
img(src="{{pathFor '/logo-header.png'}}" alt="{{currentSetting.productName}}" title="{{currentSetting.productName}}")
|
||||
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")
|
||||
|
||||
#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
|
||||
.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(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 isMiniScreen}}mobile-view{{/if}} {{#if wrappedHeader}}wrapper{{/if}}")
|
||||
#header-main-bar(class="{{#if wrappedHeader}}wrapper{{/if}}")
|
||||
+Template.dynamic(template=headerBar)
|
||||
|
||||
if appIsOffline
|
||||
|
|
@ -103,7 +128,3 @@ 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}}")
|
||||
|
|
@ -22,13 +22,13 @@ Template.header.onCreated(function () {
|
|||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.visibility = 'hidden';
|
||||
).style.display = 'none';
|
||||
else if (
|
||||
document.getElementById('headerIsSettingDatabaseCallDone') != null
|
||||
)
|
||||
document.getElementById(
|
||||
'headerIsSettingDatabaseCallDone',
|
||||
).style.visibility = 'visible';
|
||||
).style.display = 'block';
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
|
|
@ -57,6 +57,14 @@ 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) {
|
||||
|
|
@ -68,6 +76,51 @@ 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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,33 +1,7 @@
|
|||
/* 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;
|
||||
* {
|
||||
-webkit-box-sizing: unset;
|
||||
box-sizing: unset;
|
||||
}
|
||||
|
||||
: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
|
||||
*/
|
||||
|
|
@ -58,26 +32,29 @@ a:focus {
|
|||
color: unset;
|
||||
text-decoration: unset;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
gap: 0 0.3ch;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
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-family: Roboto, Poppins, "Helvetica Neue", "Liberation Sans", Arial, Helvetica, sans-serif;
|
||||
color: hsl(0, 0%, 30%);
|
||||
font: clamp(14px, 2.5vw, 18px) Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
line-height: 1.4;
|
||||
color: #4d4d4d;
|
||||
/* Improve text rendering */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
|
@ -86,74 +63,58 @@ 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%;
|
||||
overscroll-behavior: none;
|
||||
text-size-adjust: 100%;
|
||||
|
||||
}
|
||||
body {
|
||||
background: #dedede;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
z-index: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
height: 100vh;
|
||||
/* iOS Safari fixes */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
|
@ -196,6 +157,25 @@ but it worth it to let browsers take care of exact placement/sizing */
|
|||
#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 {
|
||||
|
|
@ -246,7 +226,7 @@ p {
|
|||
}
|
||||
p a {
|
||||
text-decoration: underline;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
table,
|
||||
p {
|
||||
|
|
@ -270,13 +250,13 @@ blockquote {
|
|||
padding: 0 0 0 1vw;
|
||||
}
|
||||
hr {
|
||||
height: 0.2ch;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border: none;
|
||||
width: 100%;
|
||||
background: #dbdbdb;
|
||||
color: #dbdbdb;
|
||||
margin: 0.2lh 0;
|
||||
margin: 2vh 0;
|
||||
padding: 0;
|
||||
}
|
||||
table,
|
||||
|
|
@ -323,7 +303,7 @@ kbd {
|
|||
clear: both;
|
||||
}
|
||||
.hide {
|
||||
display: none !important;
|
||||
display: none;
|
||||
}
|
||||
.show {
|
||||
display: block;
|
||||
|
|
@ -357,11 +337,8 @@ kbd {
|
|||
padding-bottom: 0;
|
||||
}
|
||||
.wrapper {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
width: auto;
|
||||
height: fit-content;
|
||||
display: grid;
|
||||
width: calc(100% - 2vw);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
|
|
@ -392,12 +369,8 @@ kbd {
|
|||
.invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.invisible-line {
|
||||
height: 1.3lh;
|
||||
visibility: hidden;
|
||||
}
|
||||
.wrapword {
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.grab {
|
||||
cursor: grab;
|
||||
|
|
@ -472,39 +445,8 @@ a:not(.disabled).is-active i.fa {
|
|||
}
|
||||
.viewer {
|
||||
min-height: 2.5vh;
|
||||
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;
|
||||
}
|
||||
display: block;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.viewer table {
|
||||
word-wrap: normal;
|
||||
|
|
@ -539,12 +481,6 @@ 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;
|
||||
}
|
||||
|
|
@ -559,7 +495,21 @@ 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;
|
||||
|
|
@ -571,18 +521,107 @@ a:not(.disabled).is-active i.fa {
|
|||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 12px 16px;
|
||||
/* Prevent zoom on iOS */
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
input, select, textarea {
|
||||
/* Prevent zoom on iOS */
|
||||
font-size: 16px; /* 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 {
|
||||
|
|
@ -596,7 +635,7 @@ a:not(.disabled).is-active i.fa {
|
|||
|
||||
/* Table mobile optimization */
|
||||
table {
|
||||
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
|
|
@ -613,6 +652,7 @@ a:not(.disabled).is-active i.fa {
|
|||
|
||||
.setting-content .content-body .side-menu {
|
||||
width: 100%;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body {
|
||||
|
|
@ -623,8 +663,94 @@ a:not(.disabled).is-active i.fa {
|
|||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
|
@ -653,19 +779,23 @@ a:not(.disabled).is-active i.fa {
|
|||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-sortable-handle {
|
||||
cursor: grab !important;
|
||||
.inline-input {
|
||||
height: 37px;
|
||||
margin: 8px 10px 0 0;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.select-authentication {
|
||||
width: 100%;
|
||||
}
|
||||
#rescue-card-description {
|
||||
.textBelowCustomLoginLogo,
|
||||
.auth-layout {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
align-self: center;
|
||||
margin: 0 0.2lh;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.auth-layout .auth-dialog {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.loadingText {
|
||||
text-align: center;
|
||||
|
|
@ -752,18 +882,8 @@ 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 {
|
||||
|
|
@ -808,11 +928,23 @@ Prevents popups to compute real size, trying to comment
|
|||
|
||||
/* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,56 +23,61 @@ template(name="main")
|
|||
//link(rel="stylesheet" type="text/css" class="__meteor-css__" href="css/html5-default-theme.css")
|
||||
|
||||
template(name="userFormsLayout")
|
||||
.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="")
|
||||
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")
|
||||
br
|
||||
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}}
|
||||
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)
|
||||
else
|
||||
if rtl
|
||||
option(value="{{tag}}") {{name}} (RTL)
|
||||
else
|
||||
option(value="{{tag}}") {{name}}
|
||||
option(value="{{tag}}" selected="selected") {{name}}
|
||||
else
|
||||
if rtl
|
||||
option(value="{{tag}}") {{name}} (RTL)
|
||||
else
|
||||
option(value="{{tag}}") {{name}}
|
||||
|
||||
template(name="defaultLayout")
|
||||
+header
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ Template.userFormsLayout.onRendered(() => {
|
|||
validator,
|
||||
);
|
||||
EscapeActions.executeAll();
|
||||
|
||||
|
||||
// Set up MutationObserver for OIDC button instead of deprecated DOMSubtreeModified
|
||||
const oidcButton = document.getElementById('at-oidc');
|
||||
if (oidcButton) {
|
||||
|
|
@ -115,7 +115,7 @@ Template.userFormsLayout.onRendered(() => {
|
|||
});
|
||||
observer.observe(oidcButton, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
|
||||
// Set up MutationObserver for .at-form instead of deprecated DOMSubtreeModified
|
||||
const atForm = document.querySelector('.at-form');
|
||||
if (atForm) {
|
||||
|
|
@ -312,9 +312,9 @@ function getAuthenticationMethod(
|
|||
if (!settings) {
|
||||
return getUserAuthenticationMethod(undefined, match);
|
||||
}
|
||||
|
||||
|
||||
const { displayAuthenticationMethod, defaultAuthenticationMethod } = settings;
|
||||
|
||||
|
||||
if (displayAuthenticationMethod) {
|
||||
return $('.select-authentication').val();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
body.mobile-mode {
|
||||
.my-cards-board-wrapper {
|
||||
width: 100vw;
|
||||
}
|
||||
.my-cards-swimlane-body {
|
||||
grid-auto-flow: row;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.my-cards-swimlane-body {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 1ch;
|
||||
.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-title {
|
||||
font-size: clamp(1em, 2.5vw, 1.3rem);
|
||||
font-size: clamp(1rem, 2.5vw, 1.3rem);
|
||||
font-weight: bold;
|
||||
padding: 0.7vh 0.7vw;
|
||||
padding-bottom: 0.5vh;
|
||||
|
|
@ -23,12 +27,48 @@ body.mobile-mode {
|
|||
.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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: clamp(300px, 20vw, 30vw);
|
||||
margin: 1.3vh 1.3vw;
|
||||
border-radius: 0.7vw;
|
||||
display: inline-grid;
|
||||
min-width: min(250px, 32vw);
|
||||
max-width: min(350px, 45vw);
|
||||
}
|
||||
|
||||
body.mobile-mode .my-cards-list-wrapper {
|
||||
max-width: unset;
|
||||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ template(name="myCardsHeaderBar")
|
|||
if currentUser
|
||||
h1
|
||||
//a.back-btn(href="{{pathFor 'home'}}")
|
||||
// i.fa.fa-chevron-left
|
||||
//
|
||||
i.fa.fa-chevron-left
|
||||
i.fa.fa-list
|
||||
| {{_ 'my-cards'}}
|
||||
|
||||
|
|
@ -39,16 +40,15 @@ template(name="myCards")
|
|||
.my-cards-swimlane-title(class="{{#if swimlane.colorClass}}{{ swimlane.colorClass }}{{else}}swimlane-default-color{{/if}}")
|
||||
+viewer
|
||||
= swimlane.title
|
||||
.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)
|
||||
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
|
||||
|
|
@ -73,7 +73,8 @@ template(name="myCards")
|
|||
.my-cards-card-title-table
|
||||
| {{card.title}}
|
||||
//a.minicard-wrapper(href=card.originRelativeUrl)
|
||||
// | {{card.title}}
|
||||
//
|
||||
| {{card.title}}
|
||||
td
|
||||
| {{list.title}}
|
||||
td
|
||||
|
|
|
|||
|
|
@ -1,121 +1,91 @@
|
|||
.pop-over {
|
||||
background: #ededed;
|
||||
background: #fff;
|
||||
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);
|
||||
/* 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;
|
||||
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;
|
||||
}
|
||||
.pop-over hr {
|
||||
margin: 0.3lh 0;
|
||||
/* below everything in the same stacking context when
|
||||
after, child or explicit z-index */
|
||||
z-index: 0;
|
||||
margin: 0.5vh 0px;
|
||||
}
|
||||
.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 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 .sub-name {
|
||||
max-width: clamp(30vw, 500px, 80%);
|
||||
.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 .header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1ch;
|
||||
align-items: center;
|
||||
padding: 0 1ch;
|
||||
height: 4.5vh;
|
||||
position: relative;
|
||||
margin-bottom: 1vh;
|
||||
background: #f7f7f7;
|
||||
border-bottom: 1px solid #dcdcdc;
|
||||
color: #666;
|
||||
min-height: 2lh;
|
||||
}
|
||||
.pop-over .header .header-title {
|
||||
display: flex;
|
||||
display: block;
|
||||
line-height: 4vh;
|
||||
padding-top: 0.5vh;
|
||||
margin: 0 1.3vw;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 1.2em;
|
||||
flex: 1;
|
||||
cursor: grab !important;
|
||||
}
|
||||
.pop-over .back-btn {
|
||||
.pop-over .header .back-btn {
|
||||
float: left;
|
||||
overflow: hidden;
|
||||
width: 4vw;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
.pop-over .back-btn.is-hidden {
|
||||
.pop-over .header .back-btn i.fa {
|
||||
margin: 1.3vw;
|
||||
margin-top: 1.5vh;
|
||||
}
|
||||
.pop-over .header .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, .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pop-over:has(.header) .content {
|
||||
/* inner content has full width available,
|
||||
so it is also responsive for margins, sizes, etc */
|
||||
.pop-over .content-wrapper {
|
||||
width: 100%;
|
||||
max-height: calc(70vh + 20px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* Allow dynamic max-height to override default constraint */
|
||||
.pop-over[style*="max-height"] .content-wrapper {
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.pop-over .content-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-height: calc(70vh + 20px);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/* Allow dynamic max-height to override default constraint for content-container */
|
||||
|
|
@ -123,42 +93,270 @@
|
|||
max-height: inherit;
|
||||
}
|
||||
|
||||
.pop-over .popup-drag-handle {
|
||||
cursor: move;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
body.mobile-mode {
|
||||
.popup-drag-handle, .close-btn {
|
||||
font-size: 1.4em;
|
||||
align-self: center;
|
||||
}
|
||||
.pop-over:has(.pop-over-list) {
|
||||
min-width: 70vw;
|
||||
}
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.pop-over .header-controls {
|
||||
display: flex;
|
||||
gap: 1ch;
|
||||
.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[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;
|
||||
|
|
@ -180,15 +378,58 @@ body.mobile-mode {
|
|||
.pop-over .content form.create-label .palette-colors {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 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 don’t 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;
|
||||
border-radius: 0;
|
||||
outline: 0.1ch solid black;
|
||||
visibility: hidden;
|
||||
}
|
||||
.pop-over.search-over {
|
||||
background: #f0f0f0;
|
||||
|
|
@ -215,6 +456,24 @@ body.mobile-mode {
|
|||
.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;
|
||||
|
|
@ -228,12 +487,15 @@ body.mobile-mode {
|
|||
cursor: pointer;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
padding-inline: 2vmin 10vmin;
|
||||
padding: 1.5px 10px;
|
||||
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;
|
||||
}
|
||||
|
|
@ -244,6 +506,7 @@ body.mobile-mode {
|
|||
.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;
|
||||
|
|
@ -259,9 +522,9 @@ body.mobile-mode {
|
|||
.pop-over-list li > a .sub-name {
|
||||
color: #8c8c8c;
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.2em;
|
||||
line-height: 15px;
|
||||
}
|
||||
.pop-over-list li > a.current {
|
||||
background-color: #e2e6e9;
|
||||
|
|
@ -307,21 +570,156 @@ body.mobile-mode {
|
|||
body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
.pop-over .content > form {
|
||||
padding: 0 1ch;
|
||||
gap: 0.2lh;
|
||||
display: flex;
|
||||
max-width: clamp(20vw, 400px, 50vw);
|
||||
.pop-over.miniprofile .header {
|
||||
border-bottom-color: transparent;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 60px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body.mobile-mode .pop-over .content>form {
|
||||
max-width: 100%;
|
||||
.pop-over.miniprofile .header-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pop-over .board-subtask-settings {
|
||||
>h3 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,696 +1,39 @@
|
|||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
||||
import { Template } from 'meteor/templating';
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
// 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);
|
||||
});
|
||||
} 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;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
24
client/components/main/popup.tpl.jade
Normal file
24
client/components/main/popup.tpl.jade
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
.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
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
height: 50px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
|
||||
font-size: 10px;
|
||||
}
|
||||
.sk-spinner-wave div {
|
||||
background-color: #333;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Template.notification.events({
|
|||
const update = {};
|
||||
const newReadValue = this.read ? null : Date.now();
|
||||
update[`profile.notifications.${this.index}.read`] = newReadValue;
|
||||
|
||||
|
||||
Users.update(Meteor.userId(), { $set: update }, (error, result) => {
|
||||
if (error) {
|
||||
console.error('Error updating notification:', error);
|
||||
|
|
@ -34,13 +34,13 @@ Template.notification.helpers({
|
|||
activityDate() {
|
||||
const activity = this.activityData;
|
||||
if (!activity || !activity.createdAt) return '';
|
||||
|
||||
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (!user) return '';
|
||||
|
||||
|
||||
const dateFormat = user.getDateFormat ? user.getDateFormat() : 'L';
|
||||
const timeFormat = user.getTimeFormat ? user.getTimeFormat() : 'LT';
|
||||
|
||||
|
||||
return moment(activity.createdAt).format(`${dateFormat} ${timeFormat}`);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ template(name='notificationIcon')
|
|||
|
||||
else if($in activityType 'createList' 'removeList' 'archivedList')
|
||||
+listNotificationIcon
|
||||
else if($in activityType 'importList')
|
||||
else if($in activityType 'importList')
|
||||
+listNotificationIcon
|
||||
//- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
|
||||
//- DRY and consistant
|
||||
|
|
|
|||
|
|
@ -1,40 +1,17 @@
|
|||
.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 {
|
||||
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 .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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,38 @@
|
|||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5lh 2ch;
|
||||
gap: 0.5lh;
|
||||
align-items: center;
|
||||
section#notifications-drawer .header {
|
||||
position: fixed;
|
||||
top: 48px;
|
||||
right: 0;
|
||||
width: calc(400px - 32px);
|
||||
padding: 8px 16px;
|
||||
background: #ededed;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
z-index: 2;
|
||||
}
|
||||
section#notifications-drawer .header .toggle-read {
|
||||
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 {
|
||||
color: #2980b9;
|
||||
}
|
||||
section#notifications-drawer .header .notification-menu {
|
||||
|
|
@ -66,13 +88,19 @@ section#notifications-drawer .header h5 {
|
|||
margin: 0;
|
||||
}
|
||||
section#notifications-drawer .header .close {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: calc(50% - 12px);
|
||||
right: 12px;
|
||||
font-size: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
section#notifications-drawer ul.notifications {
|
||||
display: block;
|
||||
padding: 0px 16px 0px 16px;
|
||||
margin: 0;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 122px);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ 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}}")
|
||||
|
|
@ -45,10 +44,9 @@ template(name='notificationsDrawer')
|
|||
span.menu-icon
|
||||
i.fa.fa-trash
|
||||
span {{_ 'delete-all-notifications'}}
|
||||
if($gt unreadNotifications 0)
|
||||
|(#{unreadNotifications}) {{_ 'notifications'}}
|
||||
else
|
||||
|0 {{_ 'notifications'}}
|
||||
h5 {{_ 'notifications'}}
|
||||
if($gt unreadNotifications 0)
|
||||
|(#{unreadNotifications})
|
||||
a.close
|
||||
i.fa.fa-times-thin
|
||||
ul.notifications
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ Template.notificationsDrawer.events({
|
|||
},
|
||||
'click .notification-menu .menu-item'(event) {
|
||||
const target = event.currentTarget;
|
||||
|
||||
|
||||
if (target.classList.contains('mark-all-read')) {
|
||||
const notifications = ReactiveCache.getCurrentUser().profile.notifications;
|
||||
for (const index in notifications) {
|
||||
|
|
|
|||
|
|
@ -85,5 +85,4 @@ template(name="setCardActionsColorPopup")
|
|||
span.card-label.palette-color.js-palette-color(class="card-details-{{color}}")
|
||||
if(isSelected color)
|
||||
i.fa.fa-check
|
||||
.form-buttons
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ template(name="checklistActions")
|
|||
select(id="check-action")
|
||||
option(value="add") {{_'r-add'}}
|
||||
option(value="remove") {{_'r-remove'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-checklist'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name",type=text,placeholder="{{_'r-name'}}")
|
||||
input(id="checklist-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-checklist-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
|
|
@ -18,10 +18,10 @@ template(name="checklistActions")
|
|||
select(id="checkall-action")
|
||||
option(value="check") {{_'r-check-all'}}
|
||||
option(value="uncheck") {{_'r-uncheck-all'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-items-check'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name2",type=text,placeholder="{{_'r-name'}}")
|
||||
input(id="checklist-name2",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-checkall-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
|
|
@ -32,32 +32,32 @@ template(name="checklistActions")
|
|||
select(id="check-item-action")
|
||||
option(value="check") {{_'r-check'}}
|
||||
option(value="uncheck") {{_'r-uncheck'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-item'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checkitem-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-of-checklist'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name3",type=text,placeholder="{{_'r-name'}}")
|
||||
input(id="checklist-name3",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-button.js-add-check-item-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-add-checklist'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-name-3",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-with-items'}}
|
||||
div.trigger-dropdown
|
||||
input(id="checklist-items",type=text,placeholder="{{_'r-items-list'}}")
|
||||
input(id="checklist-items",type=text,placeholder="{{_'r-items-list'}}")
|
||||
div.trigger-button.js-add-checklist-items-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-checklist-note'}}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,6 @@ template(name="mailActions")
|
|||
div.trigger-dropdown-mail
|
||||
input(id="email-to",type=text,placeholder="{{_'r-to'}}")
|
||||
input(id="email-subject",type=text,placeholder="{{_'r-subject'}}")
|
||||
textarea(id="email-msg")
|
||||
textarea(id="email-msg")
|
||||
div.trigger-button.trigger-button-email.js-mail-action.js-goto-rules
|
||||
i.fa.fa-plus
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ template(name="ruleDetails")
|
|||
| {{_ 'r-trigger'}}
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
= trigger
|
||||
h4
|
||||
h4
|
||||
| {{_ 'r-action'}}
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
= action
|
||||
div.trigger-text
|
||||
= action
|
||||
div.rules-back
|
||||
button.js-goback
|
||||
i.fa.fa-arrow-left
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
.triggers-content .triggers-body .triggers-side-menu {
|
||||
background-color: #f7f7f7;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
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: 0.4ch;
|
||||
border-radius: 4px;
|
||||
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: 0.4ch;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ template(name="rulesList")
|
|||
ul.rules-list
|
||||
each rules
|
||||
li.rules-lists-item
|
||||
p
|
||||
p
|
||||
= title
|
||||
div.rules-btns-group
|
||||
button.js-goto-details
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
template(name="boardTriggers")
|
||||
div.trigger-item#trigger-two
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-search
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-added-to'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-list'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-list-name",type=text,placeholder="{{_'r-list-name'}}")
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
input(id="create-swimlane-name",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
|
|
@ -29,11 +29,11 @@ template(name="boardTriggers")
|
|||
|
||||
div.trigger-item#trigger-three
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-search
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-is-moved'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
|
|
@ -47,24 +47,24 @@ template(name="boardTriggers")
|
|||
|
||||
div.trigger-item#trigger-four
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-search
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="move-action")
|
||||
option(value="moved-to") {{_'r-moved-to'}}
|
||||
option(value="moved-from") {{_'r-moved-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-list'}}
|
||||
div.trigger-dropdown
|
||||
input(id="move-list-name",type=text,placeholder="{{_'r-list-name'}}")
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-in-swimlane'}}
|
||||
div.trigger-dropdown
|
||||
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
input(id="create-swimlane-name-2",type=text,placeholder="{{_'r-swimlane-name'}}")
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
div.user-details.hide-element
|
||||
|
|
@ -77,11 +77,11 @@ template(name="boardTriggers")
|
|||
|
||||
div.trigger-item#trigger-five
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-card'}}
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
div.trigger-inline-button.js-open-card-title-popup
|
||||
i.fa.fa-search
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="arch-action")
|
||||
|
|
@ -99,7 +99,7 @@ template(name="boardTriggers")
|
|||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-board-note'}}
|
||||
|
||||
template(name="boardCardTitlePopup")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
template(name="checklistTriggers")
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-checklist'}}
|
||||
div.trigger-dropdown
|
||||
select(id="gen-check-action")
|
||||
option(value="created") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
|
|
@ -22,17 +22,17 @@ template(name="checklistTriggers")
|
|||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-the-checklist'}}
|
||||
div.trigger-dropdown
|
||||
input(id="check-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
input(id="check-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="spec-check-action")
|
||||
option(value="created") {{_'r-added-to'}}
|
||||
option(value="removed") {{_'r-removed-from'}}
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-a-card'}}
|
||||
div.trigger-button.trigger-button-person.js-show-user-field
|
||||
i.fa.fa-user
|
||||
|
|
@ -46,7 +46,7 @@ template(name="checklistTriggers")
|
|||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-checklist'}}
|
||||
div.trigger-dropdown
|
||||
select(id="gen-comp-check-action")
|
||||
|
|
@ -64,11 +64,11 @@ template(name="checklistTriggers")
|
|||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-the-checklist'}}
|
||||
div.trigger-dropdown
|
||||
input(id="spec-comp-check-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
input(id="spec-comp-check-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="spec-comp-check-action")
|
||||
|
|
@ -86,7 +86,7 @@ template(name="checklistTriggers")
|
|||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-a-item'}}
|
||||
div.trigger-dropdown
|
||||
select(id="check-item-gen-action")
|
||||
|
|
@ -104,11 +104,11 @@ template(name="checklistTriggers")
|
|||
|
||||
div.trigger-item
|
||||
div.trigger-content
|
||||
div.trigger-text
|
||||
div.trigger-text
|
||||
| {{_'r-when-the-item'}}
|
||||
div.trigger-dropdown
|
||||
input(id="check-item-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
input(id="check-item-name",type=text,placeholder="{{_'r-name'}}")
|
||||
div.trigger-text
|
||||
| {{_'r-is'}}
|
||||
div.trigger-dropdown
|
||||
select(id="check-item-spec-action")
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ template(name="attachmentSettings")
|
|||
label {{_ 'writable-path'}}
|
||||
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'attachments-path'}}
|
||||
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'avatars-path'}}
|
||||
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||
|
|
@ -30,42 +30,42 @@ template(name="attachmentSettings")
|
|||
label {{_ 's3-enabled'}}
|
||||
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-ssl-enabled'}}
|
||||
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-port'}}
|
||||
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||
|
|
@ -73,19 +73,19 @@ template(name="attachmentSettings")
|
|||
template(name="storageSettings")
|
||||
.storage-settings
|
||||
h3 {{_ 'attachment-storage-configuration'}}
|
||||
|
||||
|
||||
.storage-config-section
|
||||
h4 {{_ 'filesystem-storage'}}
|
||||
.form-group
|
||||
label {{_ 'writable-path'}}
|
||||
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'attachments-path'}}
|
||||
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'avatars-path'}}
|
||||
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||
|
|
@ -104,37 +104,37 @@ template(name="storageSettings")
|
|||
label {{_ 's3-enabled'}}
|
||||
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-ssl-enabled'}}
|
||||
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-port'}}
|
||||
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||
|
|
@ -147,18 +147,18 @@ template(name="storageSettings")
|
|||
template(name="attachmentMigration")
|
||||
.attachment-migration
|
||||
h3 {{_ 'attachment-migration'}}
|
||||
|
||||
|
||||
.migration-controls
|
||||
.form-group
|
||||
label {{_ 'migration-batch-size'}}
|
||||
input.wekan-form-control#migration-batch-size(type="number" value="{{migrationBatchSize}}" min="1" max="100")
|
||||
small.form-text.text-muted {{_ 'migration-batch-size-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'migration-delay-ms'}}
|
||||
input.wekan-form-control#migration-delay-ms(type="number" value="{{migrationDelayMs}}" min="100" max="10000")
|
||||
small.form-text.text-muted {{_ 'migration-delay-ms-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'migration-cpu-threshold'}}
|
||||
input.wekan-form-control#migration-cpu-threshold(type="number" value="{{migrationCpuThreshold}}" min="10" max="90")
|
||||
|
|
@ -169,7 +169,7 @@ template(name="attachmentMigration")
|
|||
button.js-migrate-all-to-filesystem.btn.btn-primary {{_ 'migrate-all-to-filesystem'}}
|
||||
button.js-migrate-all-to-gridfs.btn.btn-primary {{_ 'migrate-all-to-gridfs'}}
|
||||
button.js-migrate-all-to-s3.btn.btn-primary {{_ 'migrate-all-to-s3'}}
|
||||
|
||||
|
||||
.migration-controls
|
||||
button.js-pause-migration.btn.btn-warning {{_ 'pause-migration'}}
|
||||
button.js-resume-migration.btn.btn-success {{_ 'resume-migration'}}
|
||||
|
|
@ -180,7 +180,7 @@ template(name="attachmentMigration")
|
|||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
|
||||
|
||||
.migration-stats
|
||||
.stat-item
|
||||
span.label {{_ 'total-attachments'}}:
|
||||
|
|
@ -203,7 +203,7 @@ template(name="attachmentMigration")
|
|||
template(name="attachmentMonitoring")
|
||||
.attachment-monitoring
|
||||
h3 {{_ 'attachment-monitoring'}}
|
||||
|
||||
|
||||
.monitoring-stats
|
||||
.stats-grid
|
||||
.stat-card
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
border-radius: 0.4ch;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
.migration-progress {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 0.8ch;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.migration-step {
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 0.5rem rgba(102, 126, 234, 0);
|
||||
box-shadow: 0 0 0 10px 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;
|
||||
|
||||
border-radius: 0.4ch;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
|
@ -337,7 +337,7 @@
|
|||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 0.4ch;
|
||||
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
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: 0.4ch;
|
||||
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
|
|
@ -504,8 +504,8 @@
|
|||
|
||||
.form-actions .btn {
|
||||
padding: 10px 20px;
|
||||
|
||||
border-radius: 0.4ch;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
|
||||
border-radius: 0.4ch;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
|
@ -590,7 +590,7 @@
|
|||
.board-operations-stats {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 50vw;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.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: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 0.4ch;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
|
@ -776,19 +776,19 @@
|
|||
flex: 1;
|
||||
height: 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-container .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
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;
|
||||
|
||||
border-radius: 0.4ch;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
color: #333;
|
||||
|
|
@ -827,7 +827,7 @@
|
|||
|
||||
.page-info {
|
||||
color: #666;
|
||||
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
|
|
@ -846,7 +846,7 @@
|
|||
}
|
||||
|
||||
.table {
|
||||
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table th,
|
||||
|
|
@ -878,7 +878,7 @@
|
|||
#cron-setting .progress {
|
||||
height: 30px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
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: 0.4ch;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#cron-setting .progress-text {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ template(name="cronSettings")
|
|||
option(value="0") 0 - {{_ 'all-migrations'}}
|
||||
each migrationStepsWithIndex
|
||||
option(value="{{index}}") {{index}} - {{name}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
|
|
@ -18,16 +18,16 @@ template(name="cronSettings")
|
|||
.step-counter
|
||||
| Step {{migrationCurrentStepNum}}/{{migrationTotalSteps}}
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
|
||||
|
||||
.form-group
|
||||
button.js-start-migration.btn.btn-primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start'}}
|
||||
button.js-pause-migration.btn.btn-warning(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause'}}
|
||||
button.js-stop-migration.btn.btn-danger(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop'}}
|
||||
|
||||
|
||||
.form-group.migration-errors-section
|
||||
h4 {{_ 'cron-migration-errors'}}
|
||||
if hasErrors
|
||||
|
|
@ -49,7 +49,7 @@ template(name="cronSettings")
|
|||
else
|
||||
.no-errors
|
||||
| {{_ 'cron-no-errors'}}
|
||||
|
||||
|
||||
li
|
||||
h3 {{_ 'board-operations'}}
|
||||
.form-group
|
||||
|
|
@ -57,7 +57,7 @@ template(name="cronSettings")
|
|||
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
|
||||
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
|
||||
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
|
||||
|
||||
|
||||
li
|
||||
h3 {{_ 'cron-jobs'}}
|
||||
.form-group
|
||||
|
|
@ -90,22 +90,22 @@ template(name="cronMigrations")
|
|||
button.btn.btn-danger.js-stop-all-migrations
|
||||
i.fa.fa-stop
|
||||
| {{_ 'stop-all-migrations'}}
|
||||
|
||||
|
||||
.migration-progress
|
||||
.progress-overview
|
||||
.progress-bar
|
||||
.progress-fill(style="width: {{migrationProgress}}%")
|
||||
.progress-fill(style="width: {{migrationProgress}}%")
|
||||
.progress-text {{migrationProgress}}%
|
||||
.progress-label {{_ 'overall-progress'}}
|
||||
|
||||
|
||||
.current-step
|
||||
i.fa.fa-cog
|
||||
| {{migrationCurrentStep}}
|
||||
|
||||
|
||||
.migration-status
|
||||
i.fa.fa-info-circle
|
||||
| {{migrationStatus}}
|
||||
|
||||
|
||||
.migration-steps
|
||||
h3 {{_ 'migration-steps'}}
|
||||
.steps-list
|
||||
|
|
@ -149,7 +149,7 @@ template(name="cronBoardOperations")
|
|||
button.btn.btn-info.js-force-board-scan
|
||||
i.fa.fa-search
|
||||
| {{_ 'force-board-scan'}}
|
||||
|
||||
|
||||
.board-operations-stats
|
||||
.stats-grid
|
||||
.stat-item
|
||||
|
|
@ -176,7 +176,7 @@ template(name="cronBoardOperations")
|
|||
.stat-item
|
||||
.stat-value {{boardMigrationStats.isScanning}}
|
||||
.stat-label {{_ 'scanning-status'}}
|
||||
|
||||
|
||||
.system-resources
|
||||
.resource-item
|
||||
.resource-label {{_ 'cpu-usage'}}
|
||||
|
|
@ -191,18 +191,18 @@ template(name="cronBoardOperations")
|
|||
.resource-item
|
||||
.resource-label {{_ 'cpu-cores'}}
|
||||
.resource-value {{systemResources.cpuCores}}
|
||||
|
||||
|
||||
.board-operations-search
|
||||
.search-box
|
||||
input.form-control.js-search-board-operations(type="text" placeholder="{{_ 'search-boards-or-operations'}}")
|
||||
i.fa.fa-search.search-icon
|
||||
|
||||
|
||||
.board-operations-list
|
||||
.operations-header
|
||||
h3 {{_ 'board-operations'}} ({{pagination.total}})
|
||||
.pagination-info
|
||||
| {{_ 'showing'}} {{pagination.start}} - {{pagination.end}} {{_ 'of'}} {{pagination.total}}
|
||||
|
||||
|
||||
.operations-table
|
||||
table.table.table-striped
|
||||
thead
|
||||
|
|
@ -242,7 +242,7 @@ template(name="cronBoardOperations")
|
|||
i.fa.fa-stop
|
||||
button.btn.btn-sm.btn-info.js-view-details(data-operation="{{id}}")
|
||||
i.fa.fa-info-circle
|
||||
|
||||
|
||||
.pagination
|
||||
if pagination.hasPrev
|
||||
button.btn.btn-sm.btn-default.js-prev-page
|
||||
|
|
@ -265,7 +265,7 @@ template(name="cronJobs")
|
|||
button.btn.btn-success.js-refresh-jobs
|
||||
i.fa.fa-refresh
|
||||
| {{_ 'refresh'}}
|
||||
|
||||
|
||||
.jobs-list
|
||||
table.table.table-striped
|
||||
thead
|
||||
|
|
@ -304,17 +304,17 @@ template(name="cronAddJob")
|
|||
h2
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-cron-job'}}
|
||||
|
||||
|
||||
.add-job-form
|
||||
form.js-add-cron-job-form
|
||||
.form-group
|
||||
label(for="job-name") {{_ 'job-name'}}
|
||||
input.form-control#job-name(type="text" name="name" required)
|
||||
|
||||
|
||||
.form-group
|
||||
label(for="job-description") {{_ 'job-description'}}
|
||||
textarea.form-control#job-description(name="description" rows="3")
|
||||
|
||||
|
||||
.form-group
|
||||
label(for="job-schedule") {{_ 'schedule'}}
|
||||
select.form-control#job-schedule(name="schedule")
|
||||
|
|
@ -326,11 +326,11 @@ template(name="cronAddJob")
|
|||
option(value="every 6 hours") {{_ 'every-6-hours'}}
|
||||
option(value="every 1 day") {{_ 'every-1-day'}}
|
||||
option(value="once") {{_ 'run-once'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label(for="job-weight") {{_ 'weight'}}
|
||||
input.form-control#job-weight(type="number" name="weight" value="1" min="1" max="10")
|
||||
|
||||
|
||||
.form-actions
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-plus
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
.migration-progress-modal {
|
||||
background: white;
|
||||
border-radius: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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: 0.8ch;
|
||||
border-radius: 8px;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +219,7 @@
|
|||
}
|
||||
|
||||
.migration-progress-title {
|
||||
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -285,7 +285,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.migration-spinner {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ template(name="migrationProgress")
|
|||
| {{_ 'migration-progress-title'}}
|
||||
.migration-progress-close.js-close-migration-progress
|
||||
i.fa.fa-times-thin
|
||||
|
||||
|
||||
.migration-progress-content
|
||||
.migration-progress-overall
|
||||
.migration-progress-overall-label
|
||||
|
|
@ -17,7 +17,7 @@ template(name="migrationProgress")
|
|||
.migration-progress-overall-fill(style="{{progressBarStyle}}")
|
||||
.migration-progress-overall-percentage
|
||||
| {{overallProgress}}%
|
||||
|
||||
|
||||
.migration-progress-current-step
|
||||
.migration-progress-step-label
|
||||
| {{_ 'migration-progress-current-step'}}: {{stepNameFormatted}}
|
||||
|
|
@ -25,20 +25,20 @@ template(name="migrationProgress")
|
|||
.migration-progress-step-fill(style="{{stepProgressBarStyle}}")
|
||||
.migration-progress-step-percentage
|
||||
| {{stepProgress}}%
|
||||
|
||||
|
||||
.migration-progress-status
|
||||
.migration-progress-status-label
|
||||
| {{_ 'migration-progress-status'}}:
|
||||
.migration-progress-status-text
|
||||
| {{stepStatus}}
|
||||
|
||||
|
||||
if stepDetailsFormatted
|
||||
.migration-progress-details
|
||||
.migration-progress-details-label
|
||||
| {{_ 'migration-progress-details'}}:
|
||||
.migration-progress-details-text
|
||||
| {{stepDetailsFormatted}}
|
||||
|
||||
|
||||
.migration-progress-footer
|
||||
.migration-progress-note
|
||||
| {{_ 'migration-progress-note'}}
|
||||
|
|
@ -79,7 +79,7 @@ class MigrationProgressManager {
|
|||
isMigrating.set(false);
|
||||
migrationProgress.set(100);
|
||||
migrationStatus.set('Migration completed successfully!');
|
||||
|
||||
|
||||
// Clear step details after a delay
|
||||
setTimeout(() => {
|
||||
migrationStepName.set('');
|
||||
|
|
@ -178,7 +178,7 @@ Template.migrationProgress.helpers({
|
|||
stepNameFormatted() {
|
||||
const stepName = migrationStepName.get();
|
||||
if (!stepName) return '';
|
||||
|
||||
|
||||
// Convert snake_case to Title Case
|
||||
return stepName
|
||||
.split('_')
|
||||
|
|
@ -189,7 +189,7 @@ Template.migrationProgress.helpers({
|
|||
stepDetailsFormatted() {
|
||||
const details = migrationStepDetails.get();
|
||||
if (!details) return '';
|
||||
|
||||
|
||||
const formatted = [];
|
||||
for (const [key, value] of Object.entries(details)) {
|
||||
const formattedKey = key
|
||||
|
|
@ -199,7 +199,7 @@ Template.migrationProgress.helpers({
|
|||
.replace(/^\w/, c => c.toUpperCase());
|
||||
formatted.push(`${formattedKey}: ${value}`);
|
||||
}
|
||||
|
||||
|
||||
return formatted.join(', ');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ table tr:nth-child(even) {
|
|||
.ext-box button {
|
||||
min-width: 90px;
|
||||
}
|
||||
.content-wrapper {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.buttonsContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -161,7 +164,7 @@ table td:first-child {
|
|||
background-color: #27ae60;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border-radius: 0.4ch;
|
||||
border-radius: 4px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
animation: fadeOut 3s ease-in forwards;
|
||||
|
|
|
|||
|
|
@ -512,7 +512,8 @@ template(name="newUserPopup")
|
|||
span.error.hide.username-taken
|
||||
| {{_ 'error-username-taken'}}
|
||||
//if isLdap
|
||||
// input.js-profile-username(type="text" value=user.username readonly)
|
||||
//
|
||||
input.js-profile-username(type="text" value=user.username readonly)
|
||||
//else
|
||||
input.js-profile-username(type="text" value="" required)
|
||||
label
|
||||
|
|
@ -523,7 +524,8 @@ template(name="newUserPopup")
|
|||
span.error.hide.email-taken
|
||||
| {{_ 'error-email-taken'}}
|
||||
//if isLdap
|
||||
// input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
|
||||
//
|
||||
input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
|
||||
//else
|
||||
input.js-profile-email(type="email" value="" required)
|
||||
label
|
||||
|
|
@ -596,10 +598,14 @@ template(name="settingsOrgPopup")
|
|||
// It's not yet possible to impersonate organization. Only impersonate user,
|
||||
// because that changes current user ID. What would it mean in practice
|
||||
// to impersonate organization?
|
||||
// li
|
||||
// a.impersonate-org
|
||||
// i.fa.fa-user
|
||||
// | {{_ 'impersonate-org'}}
|
||||
//
|
||||
li
|
||||
//
|
||||
a.impersonate-org
|
||||
//
|
||||
i.fa.fa-user
|
||||
//
|
||||
| {{_ 'impersonate-org'}}
|
||||
//
|
||||
//
|
||||
|
||||
|
|
@ -640,8 +646,10 @@ template(name="settingsUserPopup")
|
|||
// - wekan/client/components/settings/peopleBody.jade deleteButton
|
||||
// - wekan/client/components/settings/peopleBody.js deleteButton
|
||||
// - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
|
||||
// that does now remove member from board, card members and assignees correctly,
|
||||
// but that should be used to remove user from all boards similarly
|
||||
//
|
||||
that does now remove member from board, card members and assignees correctly,
|
||||
//
|
||||
but that should be used to remove user from all boards similarly
|
||||
// - wekan/models/users.js Delete is not enabled
|
||||
|
||||
template(name="lockedUsersGeneral")
|
||||
|
|
|
|||
|
|
@ -9,32 +9,19 @@
|
|||
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;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.setting-content .wekan-form-control:not([type="radio"]) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.setting-content .content-title {
|
||||
font-size: 1.3em;
|
||||
padding: 0.5lh 1ch;
|
||||
font-size: clamp(16px, 3.5vw, 22px);
|
||||
}
|
||||
.setting-content .content-body {
|
||||
display: flex;
|
||||
padding-top: 2vh;
|
||||
height: 100%;
|
||||
gap: 1.3vw;
|
||||
}
|
||||
|
|
@ -42,15 +29,8 @@
|
|||
background-color: #f7f7f7;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 0.5vw;
|
||||
min-width: fit-content;
|
||||
width: min(250px, 32vw);
|
||||
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;
|
||||
|
|
@ -67,10 +47,12 @@
|
|||
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;
|
||||
|
|
@ -80,9 +62,9 @@
|
|||
overflow-x: scroll !important;
|
||||
overflow-y: scroll !important;
|
||||
scrollbar-gutter: stable;
|
||||
flex-grow: 5;
|
||||
padding-right: 2ch;
|
||||
padding-bottom: 1lh;
|
||||
/* Force horizontal scrollbar to always be visible */
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Ensure scrollbars are always visible with proper styling for all admin pages */
|
||||
|
|
@ -135,6 +117,24 @@
|
|||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
/* Admin panel buttons should use theme darker color */
|
||||
.setting-content .content-body .main-body .setting-detail button.btn {
|
||||
background: #005377;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body .setting-detail button.btn:hover,
|
||||
.setting-content .content-body .main-body .setting-detail button.btn:focus {
|
||||
background: #004766;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.setting-content .content-body .main-body .setting-detail button.btn:active {
|
||||
background: #01628c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Force horizontal scrollbar to always be visible at bottom */
|
||||
.setting-content .content-body .main-body {
|
||||
position: relative;
|
||||
|
|
@ -144,6 +144,7 @@
|
|||
.setting-content .content-body .main-body::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
@ -154,7 +155,7 @@
|
|||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
.setting-content .content-body .main-body ul li a .is-checked {
|
||||
border-bottom: 0.2ch solid #3cb500;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ template(name="setting")
|
|||
a.js-setting-menu(data-id="cron-settings")
|
||||
span.emoji-icon
|
||||
i.fa.fa-clock
|
||||
| {{_ 'cron'}}
|
||||
| {{_ 'migrations'}}
|
||||
.main-body
|
||||
if isLoading
|
||||
+spinner
|
||||
|
|
@ -119,12 +119,12 @@ template(name="setting")
|
|||
label {{_ 'writable-path'}}
|
||||
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'attachments-path'}}
|
||||
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 'avatars-path'}}
|
||||
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||
|
|
@ -143,49 +143,55 @@ template(name="setting")
|
|||
label {{_ 's3-enabled'}}
|
||||
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-endpoint'}}
|
||||
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-bucket'}}
|
||||
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-region'}}
|
||||
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-access-key'}}
|
||||
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-secret-key'}}
|
||||
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-ssl-enabled'}}
|
||||
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
label {{_ 's3-port'}}
|
||||
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||
|
||||
|
||||
.form-group
|
||||
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||
else if isCronSettings
|
||||
ul#cron-setting.setting-detail
|
||||
li
|
||||
h3 {{_ 'cron-migrations'}}
|
||||
h3 {{_ 'migrations'}}
|
||||
.form-group
|
||||
label {{_ 'select-migration'}}
|
||||
select.js-migration-select.wekan-form-control
|
||||
option(value="0") 0 - {{_ 'all-migrations'}}
|
||||
each migrationStepsWithIndex
|
||||
option(value="{{index}}") {{index}} - {{name}}
|
||||
.form-group
|
||||
label {{_ 'migration-status'}}
|
||||
.status-indicator
|
||||
|
|
@ -193,43 +199,45 @@ template(name="setting")
|
|||
span.status-value
|
||||
if isMigrating
|
||||
i.fa.fa-spinner.fa-spin(style="margin-right: 8px;")
|
||||
| {{#if isMigrating}}{{migrationStatus}}{{else}}{{_ 'idle'}}{{/if}}
|
||||
else if isUpdatingMigrationDropdown
|
||||
i.fa.fa-spinner.fa-spin(style="margin-right: 8px;")
|
||||
| {{#if isMigrating}}{{migrationStatusLine}}{{else}}{{migrationStatus}}{{/if}}
|
||||
if isMigrating
|
||||
.progress-section
|
||||
if migrationCurrentAction
|
||||
.step-counter
|
||||
| {{migrationCurrentAction}}
|
||||
else if migrationJobTotalSteps
|
||||
.step-counter
|
||||
| Step {{migrationJobStepNum}}/{{migrationJobTotalSteps}}
|
||||
else if migrationTotalSteps
|
||||
.step-counter
|
||||
| Migration {{migrationCurrentStepNum}}/{{migrationTotalSteps}}
|
||||
else
|
||||
.step-counter
|
||||
i.fa.fa-spinner.fa-spin(style="margin-right: 8px;")
|
||||
| Calculating migration scope...
|
||||
.progress
|
||||
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationProgress}}%
|
||||
.progress-bar(role="progressbar" style="width: {{migrationJobProgress}}%" aria-valuenow="{{migrationJobProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||
| {{migrationJobProgress}}%
|
||||
.progress-text
|
||||
| {{migrationProgress}}% {{_ 'complete'}}
|
||||
|
||||
| {{migrationJobProgress}}% {{_ 'complete'}}
|
||||
.migration-details
|
||||
if migrationJobTotalSteps
|
||||
if migrationJobTotalSteps gt 1
|
||||
.detail-line
|
||||
| Job step: {{migrationJobStepNum}}/{{migrationJobTotalSteps}}
|
||||
if migrationEtaSeconds
|
||||
.detail-line
|
||||
| ETA: {{formatDurationSeconds migrationEtaSeconds}}
|
||||
if migrationElapsedSeconds
|
||||
.detail-line
|
||||
| Elapsed: {{formatDurationSeconds migrationElapsedSeconds}}
|
||||
|
||||
.form-group
|
||||
button.js-start-all-migrations.btn.btn-primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start-all-migrations'}}
|
||||
button.js-pause-all-migrations.btn.btn-warning(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause-all-migrations'}}
|
||||
button.js-stop-all-migrations.btn.btn-danger(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop-all-migrations'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'board-operations'}}
|
||||
.form-group
|
||||
label {{_ 'scheduled-board-operations'}}
|
||||
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
|
||||
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
|
||||
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
|
||||
|
||||
li
|
||||
h3 {{_ 'cron-jobs'}}
|
||||
.form-group
|
||||
label {{_ 'active-cron-jobs'}}
|
||||
each cronJobs
|
||||
.job-item
|
||||
.job-info
|
||||
.job-name {{name}}
|
||||
.job-schedule {{schedule}}
|
||||
.job-status {{status}}
|
||||
.job-actions
|
||||
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
|
||||
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
|
||||
.add-job-section
|
||||
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
|
||||
button.js-start-migration.primary(disabled="{{#if isMigrating}}disabled{{/if}}") {{_ 'start-all-migrations'}}
|
||||
button.js-pause-all-migrations.primary(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'pause-all-migrations'}}
|
||||
button.js-stop-all-migrations.primary(disabled="{{#unless isMigrating}}disabled{{/unless}}") {{_ 'stop-all-migrations'}}
|
||||
else if isGeneralSetting
|
||||
+general
|
||||
else if isEmailSetting
|
||||
|
|
@ -285,39 +293,69 @@ template(name="general")
|
|||
template(name='email')
|
||||
ul#email-setting.setting-detail
|
||||
//if isSandstorm
|
||||
// li.smtp-form
|
||||
// .title {{_ 'smtp-host'}}
|
||||
// .description {{_ 'smtp-host-description'}}
|
||||
// .form-group
|
||||
// input.wekan-form-control#mail-server-host(type="text", placeholder="smtp.domain.com" value="{{currentSetting.mailServer.host}}")
|
||||
// li.smtp-form
|
||||
// .title {{_ 'smtp-port'}}
|
||||
// .description {{_ 'smtp-port-description'}}
|
||||
// .form-group
|
||||
// input.wekan-form-control#mail-server-port(type="text", placeholder="25" value="{{currentSetting.mailServer.port}}")
|
||||
// li.smtp-form
|
||||
// .title {{_ 'smtp-username'}}
|
||||
// .form-group
|
||||
// input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
|
||||
// li.smtp-form
|
||||
// .title {{_ 'smtp-password'}}
|
||||
// .form-group
|
||||
// input.wekan-form-control#mail-server-password(type="password", placeholder="{{_ 'password'}}" value="")
|
||||
// li.smtp-form
|
||||
// .title {{_ 'smtp-tls'}}
|
||||
// .form-group
|
||||
// a.flex.js-toggle-tls
|
||||
// .materialCheckBox#mail-server-tls(class="{{#if currentSetting.mailServer.enableTLS}}is-checked{{/if}}")
|
||||
//
|
||||
// span {{_ 'smtp-tls-description'}}
|
||||
li.smtp-form
|
||||
//
|
||||
// li.smtp-form
|
||||
// .title {{_ 'send-from'}}
|
||||
// .form-group
|
||||
// input.wekan-form-control#mail-server-from(type="email", placeholder="no-reply@domain.com" value="{{currentSetting.mailServer.from}}")
|
||||
.title {{_ 'smtp-host'}}
|
||||
//
|
||||
// li
|
||||
// button.js-save.primary {{_ 'save'}}
|
||||
.description {{_ 'smtp-host-description'}}
|
||||
//
|
||||
.form-group
|
||||
//
|
||||
input.wekan-form-control#mail-server-host(type="text", placeholder="smtp.domain.com" value="{{currentSetting.mailServer.host}}")
|
||||
//
|
||||
li.smtp-form
|
||||
//
|
||||
.title {{_ 'smtp-port'}}
|
||||
//
|
||||
.description {{_ 'smtp-port-description'}}
|
||||
//
|
||||
.form-group
|
||||
//
|
||||
input.wekan-form-control#mail-server-port(type="text", placeholder="25" value="{{currentSetting.mailServer.port}}")
|
||||
//
|
||||
li.smtp-form
|
||||
//
|
||||
.title {{_ 'smtp-username'}}
|
||||
//
|
||||
.form-group
|
||||
//
|
||||
input.wekan-form-control#mail-server-username(type="text", placeholder="{{_ 'username'}}" value="{{currentSetting.mailServer.username}}")
|
||||
//
|
||||
li.smtp-form
|
||||
//
|
||||
.title {{_ 'smtp-password'}}
|
||||
//
|
||||
.form-group
|
||||
//
|
||||
input.wekan-form-control#mail-server-password(type="password", placeholder="{{_ 'password'}}" value="")
|
||||
//
|
||||
li.smtp-form
|
||||
//
|
||||
.title {{_ 'smtp-tls'}}
|
||||
//
|
||||
.form-group
|
||||
//
|
||||
a.flex.js-toggle-tls
|
||||
//
|
||||
.materialCheckBox#mail-server-tls(class="{{#if currentSetting.mailServer.enableTLS}}is-checked{{/if}}")
|
||||
//
|
||||
//
|
||||
span {{_ 'smtp-tls-description'}}
|
||||
//
|
||||
//
|
||||
li.smtp-form
|
||||
//
|
||||
.title {{_ 'send-from'}}
|
||||
//
|
||||
.form-group
|
||||
//
|
||||
input.wekan-form-control#mail-server-from(type="email", placeholder="no-reply@domain.com" value="{{currentSetting.mailServer.from}}")
|
||||
//
|
||||
//
|
||||
li
|
||||
//
|
||||
button.js-save.primary {{_ 'save'}}
|
||||
|
||||
li
|
||||
button.js-send-smtp-test-email.primary {{_ 'send-smtp-test'}}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,23 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
|||
import { TAPi18n } from '/imports/i18n';
|
||||
import { ALLOWED_WAIT_SPINNERS } from '/config/const';
|
||||
import LockoutSettings from '/models/lockoutSettings';
|
||||
import {
|
||||
cronMigrationProgress,
|
||||
cronMigrationStatus,
|
||||
cronMigrationCurrentStep,
|
||||
cronMigrationSteps,
|
||||
cronIsMigrating,
|
||||
import {
|
||||
cronMigrationProgress,
|
||||
cronMigrationStatus,
|
||||
cronMigrationCurrentStep,
|
||||
cronMigrationSteps,
|
||||
cronIsMigrating,
|
||||
cronJobs,
|
||||
cronMigrationCurrentStepNum,
|
||||
cronMigrationTotalSteps
|
||||
cronMigrationTotalSteps,
|
||||
cronMigrationCurrentAction,
|
||||
cronMigrationJobProgress,
|
||||
cronMigrationJobStepNum,
|
||||
cronMigrationJobTotalSteps,
|
||||
cronMigrationEtaSeconds,
|
||||
cronMigrationElapsedSeconds,
|
||||
cronMigrationCurrentNumber,
|
||||
cronMigrationCurrentName
|
||||
} from '/imports/cronMigrationClient';
|
||||
|
||||
|
||||
|
|
@ -39,7 +47,7 @@ BlazeComponent.extendComponent({
|
|||
Meteor.subscribe('accessibilitySettings');
|
||||
Meteor.subscribe('globalwebhooks');
|
||||
Meteor.subscribe('lockoutSettings');
|
||||
|
||||
|
||||
// Poll for migration errors
|
||||
this.errorPollInterval = Meteor.setInterval(() => {
|
||||
if (this.cronSettings.get()) {
|
||||
|
|
@ -62,7 +70,7 @@ BlazeComponent.extendComponent({
|
|||
setError(error) {
|
||||
this.error.set(error);
|
||||
},
|
||||
|
||||
|
||||
// Template helpers moved to BlazeComponent - using different names to avoid conflicts
|
||||
isGeneralSetting() {
|
||||
return this.generalSetting && this.generalSetting.get();
|
||||
|
|
@ -102,41 +110,41 @@ BlazeComponent.extendComponent({
|
|||
filesystemPath() {
|
||||
return process.env.WRITABLE_PATH || '/data';
|
||||
},
|
||||
|
||||
|
||||
attachmentsPath() {
|
||||
const writablePath = process.env.WRITABLE_PATH || '/data';
|
||||
return `${writablePath}/attachments`;
|
||||
},
|
||||
|
||||
|
||||
avatarsPath() {
|
||||
const writablePath = process.env.WRITABLE_PATH || '/data';
|
||||
return `${writablePath}/avatars`;
|
||||
},
|
||||
|
||||
|
||||
gridfsEnabled() {
|
||||
return process.env.GRIDFS_ENABLED === 'true';
|
||||
},
|
||||
|
||||
|
||||
s3Enabled() {
|
||||
return process.env.S3_ENABLED === 'true';
|
||||
},
|
||||
|
||||
|
||||
s3Endpoint() {
|
||||
return process.env.S3_ENDPOINT || '';
|
||||
},
|
||||
|
||||
|
||||
s3Bucket() {
|
||||
return process.env.S3_BUCKET || '';
|
||||
},
|
||||
|
||||
|
||||
s3Region() {
|
||||
return process.env.S3_REGION || '';
|
||||
},
|
||||
|
||||
|
||||
s3SslEnabled() {
|
||||
return process.env.S3_SSL_ENABLED === 'true';
|
||||
},
|
||||
|
||||
|
||||
s3Port() {
|
||||
return process.env.S3_PORT || 443;
|
||||
},
|
||||
|
|
@ -145,23 +153,23 @@ BlazeComponent.extendComponent({
|
|||
migrationStatus() {
|
||||
return cronMigrationStatus.get() || TAPi18n.__('idle');
|
||||
},
|
||||
|
||||
|
||||
migrationProgress() {
|
||||
return cronMigrationProgress.get() || 0;
|
||||
},
|
||||
|
||||
|
||||
migrationCurrentStep() {
|
||||
return cronMigrationCurrentStep.get() || '';
|
||||
},
|
||||
|
||||
|
||||
isMigrating() {
|
||||
return cronIsMigrating.get() || false;
|
||||
},
|
||||
|
||||
|
||||
migrationSteps() {
|
||||
return cronMigrationSteps.get() || [];
|
||||
},
|
||||
|
||||
|
||||
migrationStepsWithIndex() {
|
||||
const steps = cronMigrationSteps.get() || [];
|
||||
return steps.map((step, idx) => ({
|
||||
|
|
@ -169,11 +177,15 @@ BlazeComponent.extendComponent({
|
|||
index: idx + 1
|
||||
}));
|
||||
},
|
||||
|
||||
|
||||
cronJobs() {
|
||||
return cronJobs.get() || [];
|
||||
},
|
||||
|
||||
isCronJobPaused(status) {
|
||||
return status === 'paused';
|
||||
},
|
||||
|
||||
migrationCurrentStepNum() {
|
||||
return cronMigrationCurrentStepNum.get() || 0;
|
||||
},
|
||||
|
|
@ -182,6 +194,52 @@ BlazeComponent.extendComponent({
|
|||
return cronMigrationTotalSteps.get() || 0;
|
||||
},
|
||||
|
||||
migrationCurrentAction() {
|
||||
return cronMigrationCurrentAction.get() || '';
|
||||
},
|
||||
|
||||
migrationJobProgress() {
|
||||
return cronMigrationJobProgress.get() || 0;
|
||||
},
|
||||
|
||||
migrationJobStepNum() {
|
||||
return cronMigrationJobStepNum.get() || 0;
|
||||
},
|
||||
|
||||
migrationJobTotalSteps() {
|
||||
return cronMigrationJobTotalSteps.get() || 0;
|
||||
},
|
||||
|
||||
migrationEtaSeconds() {
|
||||
return cronMigrationEtaSeconds.get();
|
||||
},
|
||||
|
||||
migrationElapsedSeconds() {
|
||||
return cronMigrationElapsedSeconds.get();
|
||||
},
|
||||
|
||||
migrationNumber() {
|
||||
return cronMigrationCurrentNumber.get();
|
||||
},
|
||||
|
||||
migrationName() {
|
||||
return cronMigrationCurrentName.get() || '';
|
||||
},
|
||||
|
||||
migrationStatusLine() {
|
||||
const number = cronMigrationCurrentNumber.get();
|
||||
const name = cronMigrationCurrentName.get();
|
||||
if (number && name) {
|
||||
return `${number} - ${name}`;
|
||||
}
|
||||
return this.migrationStatus();
|
||||
},
|
||||
|
||||
isUpdatingMigrationDropdown() {
|
||||
const status = this.migrationStatus();
|
||||
return status && status.startsWith('Updating Select Migration dropdown menu');
|
||||
},
|
||||
|
||||
migrationErrors() {
|
||||
return this.migrationErrorsList ? this.migrationErrorsList.get() : [];
|
||||
},
|
||||
|
|
@ -196,6 +254,19 @@ BlazeComponent.extendComponent({
|
|||
return moment(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
|
||||
formatDurationSeconds(seconds) {
|
||||
if (seconds === null || seconds === undefined) return '';
|
||||
const total = Math.max(0, Math.floor(seconds));
|
||||
const hrs = Math.floor(total / 3600);
|
||||
const mins = Math.floor((total % 3600) / 60);
|
||||
const secs = total % 60;
|
||||
const parts = [];
|
||||
if (hrs > 0) parts.push(String(hrs).padStart(2, '0'));
|
||||
parts.push(String(mins).padStart(2, '0'));
|
||||
parts.push(String(secs).padStart(2, '0'));
|
||||
return parts.join(':');
|
||||
},
|
||||
|
||||
setLoading(w) {
|
||||
this.loading.set(w);
|
||||
},
|
||||
|
|
@ -240,8 +311,14 @@ BlazeComponent.extendComponent({
|
|||
'click button.js-start-migration'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
cronIsMigrating.set(true);
|
||||
cronMigrationStatus.set(TAPi18n.__('migration-starting'));
|
||||
cronMigrationCurrentAction.set('');
|
||||
cronMigrationJobProgress.set(0);
|
||||
cronMigrationJobStepNum.set(0);
|
||||
cronMigrationJobTotalSteps.set(0);
|
||||
const selectedIndex = parseInt($('.js-migration-select').val() || '0', 10);
|
||||
|
||||
|
||||
if (selectedIndex === 0) {
|
||||
// Run all migrations
|
||||
Meteor.call('cron.startAllMigrations', (error, result) => {
|
||||
|
|
@ -258,6 +335,10 @@ BlazeComponent.extendComponent({
|
|||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else if (result && result.skipped) {
|
||||
cronIsMigrating.set(false);
|
||||
cronMigrationStatus.set(TAPi18n.__('migration-not-needed'));
|
||||
alert(TAPi18n.__('migration-not-needed'));
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-started'));
|
||||
}
|
||||
|
|
@ -265,9 +346,52 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
'click button.js-start-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.startAllMigrations', (error) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-started'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-pause-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.pauseAllMigrations', (error) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-paused'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-stop-all-migrations'(event) {
|
||||
event.preventDefault();
|
||||
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.stopAllMigrations', (error) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('migration-stopped'));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'click button.js-pause-migration'(event) {
|
||||
event.preventDefault();
|
||||
this.setLoading(true);
|
||||
cronIsMigrating.set(false);
|
||||
cronMigrationStatus.set(TAPi18n.__('migration-pausing'));
|
||||
Meteor.call('cron.pauseAllMigrations', (error, result) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
|
|
@ -282,6 +406,12 @@ BlazeComponent.extendComponent({
|
|||
event.preventDefault();
|
||||
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
||||
this.setLoading(true);
|
||||
cronIsMigrating.set(false);
|
||||
cronMigrationStatus.set(TAPi18n.__('migration-stopping'));
|
||||
cronMigrationCurrentAction.set('');
|
||||
cronMigrationJobProgress.set(0);
|
||||
cronMigrationJobStepNum.set(0);
|
||||
cronMigrationJobTotalSteps.set(0);
|
||||
Meteor.call('cron.stopAllMigrations', (error, result) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
|
|
@ -293,29 +423,25 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-cleanup'(event) {
|
||||
'click button.js-start-job'(event) {
|
||||
event.preventDefault();
|
||||
// Placeholder - board cleanup scheduling
|
||||
alert(TAPi18n.__('board-cleanup-scheduled'));
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-archive'(event) {
|
||||
event.preventDefault();
|
||||
// Placeholder - board archive scheduling
|
||||
alert(TAPi18n.__('board-archive-scheduled'));
|
||||
},
|
||||
|
||||
'click button.js-schedule-board-backup'(event) {
|
||||
event.preventDefault();
|
||||
// Placeholder - board backup scheduling
|
||||
alert(TAPi18n.__('board-backup-scheduled'));
|
||||
const jobName = $(event.target).data('job-name');
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.startJob', jobName, (error) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-start-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('cron-job-started'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-pause-job'(event) {
|
||||
event.preventDefault();
|
||||
const jobId = $(event.target).data('job-id');
|
||||
const jobName = $(event.target).data('job-name');
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.pauseJob', jobId, (error, result) => {
|
||||
Meteor.call('cron.pauseJob', jobName, (error) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
|
||||
|
|
@ -325,12 +451,26 @@ BlazeComponent.extendComponent({
|
|||
});
|
||||
},
|
||||
|
||||
'click button.js-resume-job'(event) {
|
||||
event.preventDefault();
|
||||
const jobName = $(event.target).data('job-name');
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.resumeJob', jobName, (error) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-resume-failed') + ': ' + error.reason);
|
||||
} else {
|
||||
alert(TAPi18n.__('cron-job-resumed'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click button.js-delete-job'(event) {
|
||||
event.preventDefault();
|
||||
const jobId = $(event.target).data('job-id');
|
||||
const jobName = $(event.target).data('job-name');
|
||||
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
|
||||
this.setLoading(true);
|
||||
Meteor.call('cron.removeJob', jobId, (error, result) => {
|
||||
Meteor.call('cron.removeJob', jobName, (error) => {
|
||||
this.setLoading(false);
|
||||
if (error) {
|
||||
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
|
||||
|
|
@ -429,7 +569,7 @@ BlazeComponent.extendComponent({
|
|||
$('.side-menu li.active').removeClass('active');
|
||||
target.parent().addClass('active');
|
||||
const targetID = target.data('id');
|
||||
|
||||
|
||||
// Reset all settings to false
|
||||
this.forgotPasswordSetting.set(false);
|
||||
this.generalSetting.set(false);
|
||||
|
|
@ -442,7 +582,7 @@ BlazeComponent.extendComponent({
|
|||
this.webhookSetting.set(false);
|
||||
this.attachmentSettings.set(false);
|
||||
this.cronSettings.set(false);
|
||||
|
||||
|
||||
// Set the selected setting to true
|
||||
if (targetID === 'registration-setting') {
|
||||
this.generalSetting.set(true);
|
||||
|
|
@ -847,7 +987,7 @@ BlazeComponent.extendComponent({
|
|||
const content = $('#admin-accessibility-content')
|
||||
.val()
|
||||
.trim();
|
||||
|
||||
|
||||
try {
|
||||
AccessibilitySettings.update(AccessibilitySettings.findOne()._id, {
|
||||
$set: {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue