mirror of
https://github.com/wekan/wekan.git
synced 2026-01-10 11:38:50 +01:00
Collapse Swimlane, List, Opened Card. Opened Card window X and Y position can be moved freely from drag handle. Fix some dragging not possible. Fix iPhone Safari.
Thanks to xet7 ! Fixes #6040, fixes #6027, fixes #6021, fixes #6002
This commit is contained in:
parent
95d1625a9f
commit
58f4884ad6
37 changed files with 1415 additions and 112 deletions
|
|
@ -70,4 +70,12 @@ Meteor.startup(() => {
|
|||
Meteor.subscribe('userGreyIcons');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize mobile mode on startup for iOS devices
|
||||
// This ensures mobile mode is applied correctly on page load
|
||||
Tracker.afterFlush(() => {
|
||||
if (typeof Utils !== 'undefined' && Utils.initializeUserSettings) {
|
||||
Utils.initializeUserSettings();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -231,6 +231,30 @@
|
|||
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;
|
||||
|
|
@ -263,6 +287,35 @@
|
|||
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 {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Desktop mode: hide overlay to allow multiple cards and board interaction */
|
||||
body.desktop-mode .board-wrapper .board-canvas .board-overlay {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.board-wrapper .board-canvas.is-dragging-active .open-minicard-composer,
|
||||
.board-wrapper .board-canvas.is-dragging-active .minicard-wrapper.is-checked {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,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")
|
||||
|
|
|
|||
|
|
@ -516,6 +516,16 @@ BlazeComponent.extendComponent({
|
|||
return isMiniScreen && currentCardId;
|
||||
},
|
||||
|
||||
openCards() {
|
||||
// In desktop mode, return array of all open cards
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCardIds = Session.get('openCards') || [];
|
||||
return openCardIds.map(id => ReactiveCache.getCard(id)).filter(card => card);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
goHome() {
|
||||
FlowRouter.go('home');
|
||||
},
|
||||
|
|
@ -1642,6 +1652,15 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// Open card the same way as clicking a minicard - set currentCard session
|
||||
// This shows the full card details overlay, not a popup
|
||||
// In desktop mode, add to openCards array to support multiple cards
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCards = Session.get('openCards') || [];
|
||||
if (!openCards.includes(cardId)) {
|
||||
openCards.push(cardId);
|
||||
Session.set('openCards', openCards);
|
||||
}
|
||||
}
|
||||
Session.set('currentCard', cardId);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -583,9 +583,9 @@
|
|||
}
|
||||
|
||||
.board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #2196F3;
|
||||
border-color: #2196F3;
|
||||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.6);
|
||||
background: #3cb500;
|
||||
border-color: #3cb500;
|
||||
box-shadow: 0 2px 8px rgba(60, 181, 0, 0.6);
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
top: auto !important;
|
||||
|
|
@ -601,10 +601,22 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Grey checkboxes when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-list .board-list-item .multi-selection-checkbox.is-checked {
|
||||
background: #7a7a7a;
|
||||
border-color: #7a7a7a;
|
||||
box-shadow: 0 2px 8px rgba(122, 122, 122, 0.6);
|
||||
}
|
||||
|
||||
body.grey-icons-enabled .board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #7a7a7a;
|
||||
box-shadow: 0 4px 12px rgba(122, 122, 122, 0.4);
|
||||
}
|
||||
|
||||
.board-list.is-multiselection-active .js-board.is-checked {
|
||||
outline: 4px solid #2196F3;
|
||||
outline: 4px solid #3cb500;
|
||||
outline-offset: -4px;
|
||||
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
|
||||
box-shadow: 0 4px 12px rgba(60, 181, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Visual hint when multiselection is active */
|
||||
|
|
@ -645,7 +657,11 @@
|
|||
}
|
||||
.board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
font-size: 25px;
|
||||
color: #fff;
|
||||
color: #3cb500;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .board-backgrounds-list .board-background-select .background-box i.fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
/* Prevent Grey Icons from affecting checkmarks in background color list */
|
||||
|
|
|
|||
|
|
@ -55,10 +55,9 @@ template(name="cardCustomField-number")
|
|||
template(name="cardCustomField-checkbox")
|
||||
.js-checklist-item.checklist-item(class="{{#if data.value }}is-checked{{/if}}")
|
||||
if canModifyCard
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
else
|
||||
.materialCheckBox(class="{{#if data.value }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if data.value }}✅{{else}}⬜{{/if}}
|
||||
|
||||
template(name="cardCustomField-currency")
|
||||
if canModifyCard
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ CardCustomField.register('cardCustomField');
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -118,6 +118,65 @@
|
|||
transition: flex-basis 0.1s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Desktop mode: position card below board header */
|
||||
body.desktop-mode .card-details:not(.card-details-popup) {
|
||||
position: fixed;
|
||||
width: auto;
|
||||
max-width: 800px;
|
||||
flex-basis: auto;
|
||||
border-radius: 8px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Default position for first card or when dragged */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not([style*="left"]):not([style*="top"]) {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Stagger positions for multiple cards using nth-of-type */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(1) {
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(2) {
|
||||
top: 80px;
|
||||
left: 50px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(3) {
|
||||
top: 110px;
|
||||
left: 80px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(4) {
|
||||
top: 140px;
|
||||
left: 110px;
|
||||
}
|
||||
body.desktop-mode .card-details:not(.card-details-popup):nth-of-type(5) {
|
||||
top: 170px;
|
||||
left: 140px;
|
||||
}
|
||||
|
||||
/* For expanded cards, set dimensions */
|
||||
body.desktop-mode .card-details:not(.card-details-popup):not(.card-details-collapsed) {
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
/* Collapsed card state - hide content and set height to title row only */
|
||||
.card-details.card-details-collapsed .card-details-canvas > *:not(.card-details-header) {
|
||||
display: none;
|
||||
}
|
||||
.card-details.card-details-collapsed {
|
||||
height: auto !important;
|
||||
bottom: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
body.desktop-mode .card-details.card-details-collapsed {
|
||||
bottom: auto !important;
|
||||
}
|
||||
.card-details .mCustomScrollBox {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
|
@ -139,6 +198,49 @@
|
|||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Collapse toggle triangle */
|
||||
.card-details .card-details-header .card-collapse-toggle {
|
||||
float: left;
|
||||
font-size: 20px;
|
||||
padding: 7px 10px;
|
||||
margin-left: -10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Bring to front / Send to back buttons */
|
||||
.card-details .card-details-header .card-bring-to-front,
|
||||
.card-details .card-details-header .card-send-to-back {
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
padding: 7px 8px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .card-bring-to-front:hover,
|
||||
.card-details .card-details-header .card-send-to-back:hover {
|
||||
color: #000;
|
||||
background: rgba(0,0,0,0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Drag handle */
|
||||
.card-details .card-details-header .card-drag-handle {
|
||||
font-size: 20px;
|
||||
padding: 8px 10px;
|
||||
margin-right: 10px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card-details .card-details-header .close-card-details,
|
||||
.card-details .card-details-header .maximize-card-details,
|
||||
.card-details .card-details-header .minimize-card-details,
|
||||
|
|
@ -156,11 +258,16 @@
|
|||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: -8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
.card-details .card-details-header .close-card-details-mobile-web,
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
font-size: 24px;
|
||||
padding: 5px;
|
||||
margin-right: 40px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.card-details .card-details-header .card-copy-button {
|
||||
font-size: 17px;
|
||||
|
|
@ -181,6 +288,36 @@
|
|||
padding: 10px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
.card-details .card-details-header .card-zoom-in,
|
||||
.card-details .card-details-header .card-zoom-out {
|
||||
font-size: 24px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Unify all card text to match title size */
|
||||
.card-details {
|
||||
font-size: 1em;
|
||||
}
|
||||
.card-details p,
|
||||
.card-details span,
|
||||
.card-details div,
|
||||
.card-details a,
|
||||
.card-details label,
|
||||
.card-details input,
|
||||
.card-details textarea,
|
||||
.card-details select,
|
||||
.card-details button,
|
||||
.card-details .card-details-item-title,
|
||||
.card-details .card-label,
|
||||
.card-details .viewer {
|
||||
font-size: inherit;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.card-details .card-details-header .card-details-watch {
|
||||
font-size: 17px;
|
||||
padding-left: 7px;
|
||||
|
|
@ -284,6 +421,19 @@
|
|||
position: fixed;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
/* Override for mobile mode even on larger screens */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
.card-details-maximized {
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -335,19 +485,53 @@ input[type="submit"].attachment-add-link-submit {
|
|||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.card-details {
|
||||
width: calc(100% - 1px);
|
||||
padding: 0px 20px 0px 20px;
|
||||
margin: 0px;
|
||||
width: 100% !important;
|
||||
padding: 0px 0px 0px 0px !important;
|
||||
margin: 0px !important;
|
||||
transition: none;
|
||||
overflow-y: revert;
|
||||
overflow-x: revert;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
/* iOS Safari specific fixes */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Ensure card details are above everything on mobile */
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
.card-details .card-details-canvas {
|
||||
width: 100%;
|
||||
padding-left: 0px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details {
|
||||
margin-right: 0px;
|
||||
display: block !important;
|
||||
}
|
||||
.card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-mobile-desktop-toggle {
|
||||
display: block !important;
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
.card-details .card-details-header .card-details-menu {
|
||||
margin-right: 40px;
|
||||
|
|
@ -373,6 +557,62 @@ input[type="submit"].attachment-add-link-submit {
|
|||
.pop-over > .content-wrapper > .popup-container-depth-0 .card-details-header {
|
||||
margin: 0;
|
||||
}
|
||||
/* iPhone mobile: enlarge header buttons and increase spacing */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header {
|
||||
padding-right: 16px;
|
||||
}
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .maximize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .minimize-card-details,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-menu-mobile-web,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-copy-mobile-button,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-mobile-desktop-toggle,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-in,
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-zoom-out {
|
||||
font-size: 2em !important; /* 2x bigger */
|
||||
padding: 0.3em !important;
|
||||
margin-right: 0.75em !important; /* 2x space compared to default */
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
/* Avoid clipping of the close button on the right edge */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .close-card-details {
|
||||
margin-right: 0.75em !important;
|
||||
}
|
||||
/* Enlarge the header title too */
|
||||
body.mobile-mode.iphone-device .card-details .card-details-header .card-details-title {
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile mode styles - apply when body has mobile-mode class regardless of screen size */
|
||||
body.mobile-mode .card-details {
|
||||
width: 100vw !important;
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 100 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-canvas {
|
||||
width: 100% !important;
|
||||
padding: 0 15px !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details,
|
||||
body.mobile-mode .card-details .card-details-header .close-card-details-mobile-web {
|
||||
display: block !important;
|
||||
}
|
||||
.card-details-white {
|
||||
background: #fff !important;
|
||||
|
|
|
|||
|
|
@ -5,16 +5,25 @@ template(name="cardDetails")
|
|||
|
||||
+attachmentViewer
|
||||
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}'): .card-details-canvas
|
||||
section.card-details.js-card-details.nodragscroll(class='{{#if cardMaximized}}card-details-maximized{{/if}}' class='{{#if isPopup}}card-details-popup{{/if}}' class='{{#unless isVerticalScrollbars}}no-scrollbars{{/unless}}' class='{{#if cardCollapsed}}card-details-collapsed{{/if}}'): .card-details-canvas
|
||||
.card-details-header(class='{{#if colorClass}}card-details-{{colorClass}}{{/if}}')
|
||||
+inlinedForm(classNames="js-card-details-title")
|
||||
+editCardTitleForm
|
||||
else
|
||||
unless isMiniScreen
|
||||
unless isPopup
|
||||
span.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
|
||||
if cardCollapsed
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
if canModifyCard
|
||||
a.card-bring-to-front.js-card-bring-to-front(title="Bring to front")
|
||||
| ⏫
|
||||
a.card-send-to-back.js-card-send-to-back(title="Send to back")
|
||||
| ⏬
|
||||
if cardMaximized
|
||||
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
| 🔽
|
||||
|
|
@ -30,12 +39,28 @@ template(name="cardDetails")
|
|||
href="{{ originRelativeUrl }}"
|
||||
)
|
||||
span.emoji-icon 🔗
|
||||
span.card-drag-handle.js-card-drag-handle(title="Drag card")
|
||||
| ↕️
|
||||
span.copied-tooltip {{_ 'copied'}}
|
||||
else
|
||||
unless isPopup
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
|
||||
| ❌
|
||||
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
|
||||
| 🔍➖
|
||||
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
|
||||
| 🔍➕
|
||||
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
|
||||
if mobileMode
|
||||
| 🖥️
|
||||
else
|
||||
| 📱
|
||||
if canModifyCard
|
||||
if cardMaximized
|
||||
a.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
|
||||
| 🔽
|
||||
else
|
||||
a.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
|
||||
| 🔼
|
||||
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
|
||||
| ☰
|
||||
a.card-copy-mobile-button.js-copy-link(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,11 @@ BlazeComponent.extendComponent({
|
|||
const boardBody = this.parentComponent().parentComponent();
|
||||
//in Miniview parent is Board, not BoardBody.
|
||||
if (boardBody !== null) {
|
||||
boardBody.showOverlay.set(true);
|
||||
// Only show overlay in mobile mode, not in desktop mode
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (isMobile) {
|
||||
boardBody.showOverlay.set(true);
|
||||
}
|
||||
boardBody.mouseHasEnterCardDetails = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +97,18 @@ BlazeComponent.extendComponent({
|
|||
return !Utils.getPopupCardId() && ReactiveCache.getCurrentUser().hasCardMaximized();
|
||||
},
|
||||
|
||||
cardCollapsed() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile) {
|
||||
return !!user.profile.cardCollapsed;
|
||||
}
|
||||
if (Users.getPublicCardCollapsed) {
|
||||
const stored = Users.getPublicCardCollapsed();
|
||||
if (typeof stored === 'boolean') return stored;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
presentParentTask() {
|
||||
let result = this.currentBoard.presentParentTask;
|
||||
if (result === null || result === undefined) {
|
||||
|
|
@ -296,13 +312,88 @@ BlazeComponent.extendComponent({
|
|||
return [
|
||||
{
|
||||
...events,
|
||||
'click .js-card-collapse-toggle'() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const currentState = user && user.profile ? !!user.profile.cardCollapsed : !!Users.getPublicCardCollapsed();
|
||||
if (user) {
|
||||
Meteor.call('setCardCollapsed', !currentState);
|
||||
} else if (Users.setPublicCardCollapsed) {
|
||||
Users.setPublicCardCollapsed(!currentState);
|
||||
}
|
||||
},
|
||||
'click .js-card-bring-to-front'(event) {
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
// Find the highest z-index among all cards
|
||||
let maxZ = 100;
|
||||
$('.card-details').each(function() {
|
||||
const z = parseInt($(this).css('z-index')) || 100;
|
||||
if (z > maxZ) maxZ = z;
|
||||
});
|
||||
// Set this card's z-index to be higher
|
||||
$card.css('z-index', maxZ + 1);
|
||||
},
|
||||
'click .js-card-send-to-back'(event) {
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
// Find the lowest z-index among all cards
|
||||
let minZ = 100;
|
||||
$('.card-details').each(function() {
|
||||
const z = parseInt($(this).css('z-index')) || 100;
|
||||
if (z < minZ) minZ = z;
|
||||
});
|
||||
// Set this card's z-index to be lower
|
||||
$card.css('z-index', minZ - 1);
|
||||
},
|
||||
'mousedown .js-card-drag-handle'(event) {
|
||||
event.preventDefault();
|
||||
const $card = $(event.target).closest('.card-details');
|
||||
const startX = event.clientX;
|
||||
const startY = event.clientY;
|
||||
const startLeft = $card.offset().left;
|
||||
const startTop = $card.offset().top;
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
const deltaX = e.clientX - startX;
|
||||
const deltaY = e.clientY - startY;
|
||||
$card.css({
|
||||
left: startLeft + deltaX + 'px',
|
||||
top: startTop + deltaY + 'px'
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
$(document).off('mousemove', onMouseMove);
|
||||
$(document).off('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
$(document).on('mousemove', onMouseMove);
|
||||
$(document).on('mouseup', onMouseUp);
|
||||
},
|
||||
'click .js-close-card-details'() {
|
||||
// Get board ID from either the card data or current board in session
|
||||
const card = this.currentData() || this.data();
|
||||
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
|
||||
const cardId = card && card._id;
|
||||
|
||||
if (boardId) {
|
||||
// Clear the current card session to close the card
|
||||
// In desktop mode, remove from openCards array
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile && cardId) {
|
||||
const openCards = Session.get('openCards') || [];
|
||||
const filtered = openCards.filter(id => id !== cardId);
|
||||
Session.set('openCards', filtered);
|
||||
|
||||
// If this was the current card, clear it
|
||||
if (Session.get('currentCard') === cardId) {
|
||||
Session.set('currentCard', null);
|
||||
}
|
||||
|
||||
// Don't navigate away in desktop mode - just close the card
|
||||
return;
|
||||
}
|
||||
|
||||
// Mobile mode: Clear the current card session to close the card
|
||||
Session.set('currentCard', null);
|
||||
|
||||
// Navigate back to board without card
|
||||
|
|
@ -327,6 +418,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);
|
||||
},
|
||||
'submit .js-card-description'(event) {
|
||||
event.preventDefault();
|
||||
const description = this.currentComponent().getValue();
|
||||
|
|
|
|||
|
|
@ -37,14 +37,23 @@ textarea.js-edit-checklist-item {
|
|||
.checklist-progress-bar-container .checklist-progress-bar {
|
||||
width: 80%;
|
||||
height: 10px;
|
||||
background-color: #d6ebff !important;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
color: #fff !important;
|
||||
background-color: #2196f3 !important;
|
||||
background-color: #3cb500 !important;
|
||||
padding: 0.01em 16px;
|
||||
border-radius: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
/* Grey progress bar when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-progress-bar-container .checklist-progress-bar .checklist-progress {
|
||||
background-color: #7a7a7a !important;
|
||||
}
|
||||
.checklist-title {
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
@ -105,6 +114,25 @@ textarea.js-edit-checklist-item {
|
|||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* iPhone mobile: larger checklist titles and more spacing between items */
|
||||
body.mobile-mode.iphone-device .checklist-title .title {
|
||||
font-size: 1.3em !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item {
|
||||
margin-top: 12px !important;
|
||||
margin-bottom: 8px !important;
|
||||
padding: 8px 4px !important;
|
||||
min-height: 44px; /* iOS recommended touch target size */
|
||||
}
|
||||
|
||||
body.mobile-mode.iphone-device .checklist-item span.checklistitem-handle {
|
||||
font-size: 1.5em !important;
|
||||
padding-right: 15px !important;
|
||||
width: 1.5em !important;
|
||||
}
|
||||
.checklist-item.is-checked.invisible {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
|
|
@ -134,6 +162,27 @@ textarea.js-edit-checklist-item {
|
|||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.checklist-item .check-box-unicode,
|
||||
.cardCustomField-checkbox .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .checklist-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .checklist-item .check-box-unicode,
|
||||
body.grey-icons-enabled .cardCustomField-checkbox .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.checklist-item .item-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -155,6 +204,7 @@ textarea.js-edit-checklist-item {
|
|||
width: 1.2em;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
.js-delete-checklist-item,
|
||||
.js-convert-checklist-item-to-card {
|
||||
|
|
|
|||
|
|
@ -125,14 +125,13 @@ 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")
|
||||
if canModifyCard
|
||||
.check-box-container
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ BlazeComponent.extendComponent({
|
|||
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
|
||||
if (Utils.isTouchScreenOrShowDesktopDragHandles()) {
|
||||
$(self.itemsDom).sortable({
|
||||
handle: 'span.fa.checklistitem-handle',
|
||||
handle: 'span.checklistitem-handle',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -360,6 +360,7 @@ BlazeComponent.extendComponent({
|
|||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-checklist-item .check-box-unicode': this.toggleItem,
|
||||
'click .js-checklist-item .check-box-container': this.toggleItem,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -87,6 +87,15 @@ textarea.js-edit-subtask-item {
|
|||
top: 0;
|
||||
bottom: -600px;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
/* Fix for mobile Safari: ensure this doesn't block card interaction */
|
||||
@media screen and (max-width: 800px) {
|
||||
#card-details-overlay {
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.subtasks {
|
||||
background: #f7f7f7;
|
||||
|
|
@ -127,6 +136,25 @@ textarea.js-edit-subtask-item {
|
|||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Unicode checkbox icons styling */
|
||||
.subtasks-item .check-box-unicode {
|
||||
font-size: 1.3em;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .subtasks-item .check-box.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
body.grey-icons-enabled .subtasks-item .check-box-unicode {
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.subtasks-item .item-title {
|
||||
flex: 1;
|
||||
padding-left: 10px;
|
||||
|
|
|
|||
|
|
@ -74,12 +74,12 @@ template(name="subtasksItems")
|
|||
template(name='subtaskItemDetail')
|
||||
.js-subtasks-item.subtasks-item
|
||||
if canModifyCard
|
||||
.check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
else
|
||||
.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
span.check-box-unicode {{#if item.isFinished }}✅{{else}}⬜{{/if}}
|
||||
.item-title(class="{{#if item.isFinished }}is-checked{{/if}}")
|
||||
+viewer
|
||||
= item.title
|
||||
|
|
|
|||
|
|
@ -104,7 +104,19 @@ BlazeComponent.extendComponent({
|
|||
}).register('subtasks');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
// ...
|
||||
toggleItem() {
|
||||
const item = this.currentData().item;
|
||||
if (item && item._id) {
|
||||
item.toggleItem();
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'click .js-subtasks-item .check-box-unicode': this.toggleItem,
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('subtaskItemDetail');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
|
|
|
|||
|
|
@ -315,11 +315,18 @@ textarea::-moz-placeholder {
|
|||
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-origin: 100% 100%;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
.button-link {
|
||||
background: #fff;
|
||||
background: linear-gradient(#fff, #f5f5f5);
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ body.list-resizing-active * {
|
|||
margin: 0 auto;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
margin: 0 auto 20px auto;
|
||||
margin: 0 auto 0 auto;
|
||||
z-index: 10;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
|
|
@ -290,6 +290,12 @@ body.list-resizing-active * {
|
|||
display: block;
|
||||
width: fit-content;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-header-handle {
|
||||
position: absolute !important;
|
||||
top: 30px !important;
|
||||
right: 1.5vw !important;
|
||||
z-index: 15 !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
|
|
@ -297,7 +303,6 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
|
|
@ -308,15 +313,15 @@ body.list-resizing-active * {
|
|||
color: #333;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px 4px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
margin: 0 auto;
|
||||
width: 25vh;
|
||||
height: 60vh;
|
||||
margin: 0;
|
||||
width: 100vh;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
transform: translateY(calc(-50% + 20px)) rotate(0deg);
|
||||
z-index: 10;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
|
|
@ -415,22 +420,42 @@ body.list-resizing-active * {
|
|||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
}
|
||||
/* List header collapse button styling */
|
||||
.list-header .list-header-collapse-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-header .js-collapse {
|
||||
color: #a6a6a6;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
}
|
||||
.list-header .js-collapse:hover {
|
||||
background-color: #e0e0e0;
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-header .list-header-collapse-container > div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.list.list-collapsed .list-header .js-collapse {
|
||||
display: inline-block !important;
|
||||
visibility: visible !important;
|
||||
|
|
@ -459,17 +484,18 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 15vh;
|
||||
width: 100vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
|
|
@ -499,17 +525,18 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 15vh;
|
||||
width: 100vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
text-align: left;
|
||||
transform: translateY(calc(-50% + 120px)) rotate(0deg);
|
||||
text-align: center;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
display: block !important;
|
||||
|
|
@ -539,16 +566,17 @@ body.list-resizing-active * {
|
|||
position: relative !important;
|
||||
}
|
||||
.list.list-collapsed .list-header .list-rotated h2.list-header-name {
|
||||
width: 15vh;
|
||||
width: 100vh;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
line-height: 1.2;
|
||||
padding: 8px 4px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: 40px;
|
||||
top: 50%;
|
||||
transform: translate(calc(-50% + 50px), -50%) rotate(0deg);
|
||||
transform: translateY(calc(-50% + 40px)) rotate(0deg);
|
||||
text-align: left;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
|
|
@ -1053,6 +1081,23 @@ body.list-resizing-active * {
|
|||
grid-row: 1/3 !important;
|
||||
grid-column: 1 !important;
|
||||
}
|
||||
|
||||
/* Allow long list titles to expand on desktop (non-mobile, non-collapsed) */
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.list:not(.mobile-view):not(.list-collapsed) .list-header .list-header-name {
|
||||
/* Permit wrapping and full visibility */
|
||||
white-space: normal !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
display: inline-block !important;
|
||||
/* Reserve space for right-side controls (menu, handle, count) */
|
||||
max-width: calc(100% - 120px) !important;
|
||||
/* Break long words to avoid overflow */
|
||||
word-break: break-word !important;
|
||||
}
|
||||
.link-board-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@ template(name='list')
|
|||
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
|
||||
+listBody
|
||||
.list-resize-handle.js-list-resize-handle.nodragscroll
|
||||
unless collapsed
|
||||
+listBody
|
||||
.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}}")
|
||||
|
|
|
|||
|
|
@ -279,7 +279,8 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// Only enable resize for non-collapsed, non-auto-width lists
|
||||
const isAutoWidth = this.autoWidth();
|
||||
if (list.collapsed || isAutoWidth) {
|
||||
const isCollapsed = Utils.getListCollapseState(list);
|
||||
if (isCollapsed || isAutoWidth) {
|
||||
$resizeHandle.hide();
|
||||
return;
|
||||
}
|
||||
|
|
@ -433,9 +434,10 @@ BlazeComponent.extendComponent({
|
|||
});
|
||||
|
||||
|
||||
// Reactively update resize handle visibility when auto-width changes
|
||||
// Reactively update resize handle visibility when auto-width or collapse changes
|
||||
component.autorun(() => {
|
||||
if (component.autoWidth()) {
|
||||
const collapsed = Utils.getListCollapseState(list);
|
||||
if (component.autoWidth() || collapsed) {
|
||||
$resizeHandle.hide();
|
||||
} else {
|
||||
$resizeHandle.show();
|
||||
|
|
@ -452,6 +454,12 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
}).register('list');
|
||||
|
||||
Template.list.helpers({
|
||||
collapsed() {
|
||||
return Utils.getListCollapseState(this);
|
||||
},
|
||||
});
|
||||
|
||||
Template.miniList.events({
|
||||
'click .js-select-list'() {
|
||||
const listId = this._id;
|
||||
|
|
|
|||
|
|
@ -30,20 +30,22 @@ template(name="listHeader")
|
|||
|
|
||||
span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
|
||||
else
|
||||
if collapsed
|
||||
a.js-collapse(title="{{_ 'uncollapse'}}")
|
||||
| ⬅️
|
||||
| ➡️
|
||||
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}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
div.list-header-collapse-container
|
||||
a.list-collapse-indicator.js-collapse(title="{{_ 'collapse'}}")
|
||||
if collapsed
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
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}}")
|
||||
+viewer
|
||||
= title
|
||||
if wipLimit.enabled
|
||||
| (
|
||||
span(class="{{#if exceededWipLimit}}highlight{{/if}}") {{cards.length}}
|
||||
|/#{wipLimit.value})
|
||||
unless collapsed
|
||||
if showCardsCountForList cards.length
|
||||
span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
|
||||
|
|
@ -64,6 +66,10 @@ template(name="listHeader")
|
|||
unless currentUser.isWorker
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
else if currentUser.isBoardMember
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
if isWatching
|
||||
i.list-header-watch-icon | 👁️
|
||||
unless collapsed
|
||||
|
|
@ -73,14 +79,8 @@ template(name="listHeader")
|
|||
// a.fa.js-list-star.list-header-plus-top(class="fa-star{{#unless starred}}-o{{/unless}}")
|
||||
if canSeeAddCard
|
||||
a.js-add-card.list-header-plus-top(title="{{_ 'add-card-to-top-of-list'}}") ➕
|
||||
a.js-collapse(title="{{_ 'collapse'}}")
|
||||
| ⬅️
|
||||
| ➡️
|
||||
|
||||
a.js-open-list-menu(title="{{_ 'listActionPopup-title'}}") ☰
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.list-header-handle.handle.js-list-handle ↕️
|
||||
|
||||
template(name="editListTitleForm")
|
||||
.list-composer
|
||||
|
|
|
|||
|
|
@ -34,13 +34,14 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
collapsed(check = undefined) {
|
||||
const list = Template.currentData();
|
||||
const status = list.isCollapsed();
|
||||
const status = Utils.getListCollapseState(list);
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
list.collapse(!status);
|
||||
return !status;
|
||||
const next = typeof check === 'boolean' ? check : !status;
|
||||
Utils.setListCollapseState(list, next);
|
||||
return next;
|
||||
}
|
||||
},
|
||||
editTitle(event) {
|
||||
|
|
|
|||
|
|
@ -339,15 +339,20 @@
|
|||
width: 100%;
|
||||
min-width: 3vw;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
box-sizing: border-box;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Make zoom input wider on all mobile screens */
|
||||
@media screen and (max-width: 800px),
|
||||
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
|
||||
#header-quick-access .zoom-controls .zoom-input {
|
||||
min-width: 50px !important; /* Wider on mobile */
|
||||
width: 50px !important; /* Fixed width to show all numbers */
|
||||
font-size: 14px !important; /* Slightly larger text */
|
||||
min-width: 80px !important; /* Wider on mobile to show 3 digits */
|
||||
width: 80px !important; /* Fixed width to show 100 fully */
|
||||
font-size: 16px !important; /* Slightly larger text */
|
||||
flex: 0 0 80px !important; /* Prevent shrinking in flex */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -850,8 +855,9 @@
|
|||
#header-quick-access .zoom-controls .zoom-input {
|
||||
font-size: 16px !important; /* Larger input text */
|
||||
padding: 0.5vh 0.8vw !important;
|
||||
min-width: 6vw !important; /* Much wider for mobile */
|
||||
width: 60px !important; /* Fixed width to show all numbers */
|
||||
min-width: 80px !important; /* Wider to fit 100 */
|
||||
width: 80px !important; /* Fixed width to show 100 fully */
|
||||
flex: 0 0 80px !important; /* Prevent shrinking in flex */
|
||||
}
|
||||
|
||||
/* Make mobile mode toggle larger */
|
||||
|
|
|
|||
|
|
@ -81,6 +81,27 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
height: calc(100vh - 48px);
|
||||
}
|
||||
#content {
|
||||
position: relative;
|
||||
|
|
@ -899,6 +920,40 @@ a:not(.disabled).is-active i.fa {
|
|||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS Safari Mobile Mode Fixes */
|
||||
@media screen and (max-width: 800px) {
|
||||
/* Prevent scrolling issues on iOS Safari when card popup is open */
|
||||
body.mobile-mode {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Fix z-index stacking for mobile Safari */
|
||||
body.mobile-mode .board-wrapper {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
body.mobile-mode .board-wrapper .board-canvas .board-overlay {
|
||||
z-index: 17 !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .card-details {
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
body.mobile-mode .pop-over {
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Ensure smooth scrolling on iOS */
|
||||
body.mobile-mode .card-details,
|
||||
body.mobile-mode .pop-over .content-wrapper {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
@-moz-keyframes lds-roller {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ template(name="main")
|
|||
html(lang="{{TAPi18n.getLanguage}}")
|
||||
head
|
||||
title
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes")
|
||||
meta(name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes, viewport-fit=cover")
|
||||
meta(http-equiv="X-UA-Compatible" content="IE=edge")
|
||||
meta(name="apple-mobile-web-app-capable" content="yes")
|
||||
meta(name="apple-mobile-web-app-status-bar-style" content="black-translucent")
|
||||
//- XXX We should use pathFor in the following `href` to support the case
|
||||
where the application is deployed with a path prefix, but it seems to be
|
||||
difficult to do that cleanly with Blaze -- at least without adding extra
|
||||
|
|
|
|||
|
|
@ -538,6 +538,7 @@
|
|||
position: absolute;
|
||||
top: 6px;
|
||||
right: 12px;
|
||||
color: #3cb500;
|
||||
}
|
||||
.pop-over-list .pop-over-list.checkable li.active a {
|
||||
padding-right: 28px;
|
||||
|
|
@ -545,6 +546,10 @@
|
|||
.pop-over-list .pop-over-list.checkable li.active a .fa-check {
|
||||
display: block;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .pop-over-list .pop-over-list.checkable .fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
.pop-over.miniprofile .header {
|
||||
border-bottom-color: transparent;
|
||||
height: 30px;
|
||||
|
|
@ -590,6 +595,10 @@
|
|||
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;
|
||||
|
|
@ -674,3 +683,23 @@
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,8 +137,13 @@
|
|||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
.setting-content .content-body .main-body ul li a .is-checked {
|
||||
border-bottom: 2px solid #2980b9;
|
||||
border-right: 2px solid #2980b9;
|
||||
border-bottom: 2px solid #3cb500;
|
||||
border-right: 2px solid #3cb500;
|
||||
}
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .setting-content .content-body .main-body ul li a .is-checked {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
.setting-content .content-body .main-body ul li a span {
|
||||
padding: 0 0.5rem;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@
|
|||
transform-origin: 100% 100% !important;
|
||||
}
|
||||
|
||||
/* Grey checkmarks when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .sidebar .materialCheckBox.is-checked,
|
||||
body.grey-icons-enabled .boardCardSettingsPopup .materialCheckBox.is-checked,
|
||||
body.grey-icons-enabled .boardSubtaskSettingsPopup .materialCheckBox.is-checked {
|
||||
border-bottom: 2px solid #7a7a7a !important;
|
||||
border-right: 2px solid #7a7a7a !important;
|
||||
}
|
||||
|
||||
/* Card Settings 3-column grid layout */
|
||||
.card-settings-grid {
|
||||
display: grid;
|
||||
|
|
@ -130,6 +138,11 @@
|
|||
}
|
||||
.sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
|
||||
margin: 0 4px;
|
||||
color: #3cb500;
|
||||
}
|
||||
/* Grey check icons when grey icons setting is enabled */
|
||||
body.grey-icons-enabled .sidebar .sidebar-content ul.sidebar-list li > a .fa.fa-check {
|
||||
color: #7a7a7a;
|
||||
}
|
||||
.sidebar .sidebar-content ul.sidebar-list li .minicard {
|
||||
padding: 6px 8px 4px;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ template(name="swimlaneFixedHeader")
|
|||
if currentUser
|
||||
unless currentUser.isCommentOnly
|
||||
unless currentUser.isWorker
|
||||
a.swimlane-collapse-indicator.js-collapse-swimlane.swimlane-header-collapse(title="{{_ 'collapse'}}")
|
||||
if collapseSwimlane
|
||||
| ▶
|
||||
else
|
||||
| 🔽
|
||||
a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
|
||||
| ➕
|
||||
unless isTouchScreen
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@ BlazeComponent.extendComponent({
|
|||
},
|
||||
collapsed(check = undefined) {
|
||||
const swimlane = Template.currentData();
|
||||
const status = swimlane.isCollapsed();
|
||||
const status = Utils.getSwimlaneCollapseState(swimlane);
|
||||
if (check === undefined) {
|
||||
// just check
|
||||
return status;
|
||||
} else {
|
||||
swimlane.collapse(!status);
|
||||
return !status;
|
||||
const next = typeof check === 'boolean' ? check : !status;
|
||||
Utils.setSwimlaneCollapseState(swimlane, next);
|
||||
return next;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -49,6 +50,10 @@ Template.swimlaneFixedHeader.helpers({
|
|||
isBoardAdmin() {
|
||||
return ReactiveCache.getCurrentUser().isBoardAdmin();
|
||||
},
|
||||
collapseSwimlane() {
|
||||
const swimlane = Template.currentData();
|
||||
return Utils.getSwimlaneCollapseState(swimlane);
|
||||
},
|
||||
isTitleDefault(title) {
|
||||
// https://github.com/wekan/wekan/issues/4763
|
||||
// https://github.com/wekan/wekan/issues/4742
|
||||
|
|
|
|||
|
|
@ -130,6 +130,29 @@
|
|||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Swimlane collapse button styling - matches list collapse button */
|
||||
.swimlane .swimlane-header-wrap .swimlane-header-menu .swimlane-collapse-indicator {
|
||||
color: #a6a6a6;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
min-width: 30px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.swimlane .swimlane-header-wrap .swimlane-header-menu .swimlane-collapse-indicator:hover {
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#js-swimlane-height-edit .swimlane-height-error {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,6 +283,9 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// Wait for DOM to be ready
|
||||
setTimeout(() => {
|
||||
const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles()
|
||||
? '.js-list-handle'
|
||||
: '.js-list-header';
|
||||
const $lists = this.$('.js-list');
|
||||
|
||||
const $parent = $lists.parent();
|
||||
|
|
@ -306,7 +309,7 @@ BlazeComponent.extendComponent({
|
|||
items: '.js-list:not(.js-list-composer)',
|
||||
placeholder: 'list placeholder',
|
||||
distance: 7,
|
||||
handle: '.js-list-handle',
|
||||
handle: handleSelector,
|
||||
disabled: !Utils.canModifyBoard(),
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
|
|
@ -319,6 +322,15 @@ BlazeComponent.extendComponent({
|
|||
boardComponent.setIsDragging(false);
|
||||
}
|
||||
});
|
||||
// Reactively update handle when user toggles desktop drag handles
|
||||
this.autorun(() => {
|
||||
const newHandle = Utils.isTouchScreenOrShowDesktopDragHandles()
|
||||
? '.js-list-handle'
|
||||
: '.js-list-header';
|
||||
if ($parent.data('uiSortable') || $parent.data('sortable')) {
|
||||
try { $parent.sortable('option', 'handle', newHandle); } catch (e) {}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
}
|
||||
}, 100);
|
||||
|
|
@ -684,6 +696,10 @@ Template.swimlane.helpers({
|
|||
lists() {
|
||||
// Return per-swimlane lists for this swimlane
|
||||
return this.myLists();
|
||||
},
|
||||
|
||||
collapseSwimlane() {
|
||||
return Utils.getSwimlaneCollapseState(this);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -691,6 +707,9 @@ Template.swimlane.helpers({
|
|||
setTimeout(() => {
|
||||
const $swimlaneElements = $('.swimlane');
|
||||
const $listsGroupElements = $('.list-group');
|
||||
const computeHandle = () => (
|
||||
Utils.isTouchScreenOrShowDesktopDragHandles() ? '.js-list-handle' : '.js-list-header'
|
||||
);
|
||||
|
||||
// Initialize sortable on ALL swimlane elements (even empty ones)
|
||||
$swimlaneElements.each(function(index) {
|
||||
|
|
@ -707,7 +726,7 @@ setTimeout(() => {
|
|||
items: '.js-list:not(.js-list-composer)',
|
||||
placeholder: 'list placeholder',
|
||||
distance: 7,
|
||||
handle: '.js-list-handle',
|
||||
handle: computeHandle(),
|
||||
disabled: !Utils.canModifyBoard(),
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
|
|
@ -831,6 +850,13 @@ setTimeout(() => {
|
|||
});
|
||||
}
|
||||
});
|
||||
// Reactively adjust handle when setting changes
|
||||
Tracker.autorun(() => {
|
||||
const newHandle = computeHandle();
|
||||
if ($swimlane.data('uiSortable') || $swimlane.data('sortable')) {
|
||||
try { $swimlane.sortable('option', 'handle', newHandle); } catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -849,7 +875,7 @@ setTimeout(() => {
|
|||
items: '.js-list:not(.js-list-composer)',
|
||||
placeholder: 'list placeholder',
|
||||
distance: 7,
|
||||
handle: '.js-list-handle',
|
||||
handle: computeHandle(),
|
||||
disabled: !Utils.canModifyBoard(),
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
|
|
@ -973,6 +999,13 @@ setTimeout(() => {
|
|||
});
|
||||
}
|
||||
});
|
||||
// Reactively adjust handle when setting changes
|
||||
Tracker.autorun(() => {
|
||||
const newHandle = computeHandle();
|
||||
if ($listsGroup.data('uiSortable') || $listsGroup.data('sortable')) {
|
||||
try { $listsGroup.sortable('option', 'handle', newHandle); } catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
|
@ -1018,6 +1051,9 @@ BlazeComponent.extendComponent({
|
|||
|
||||
// Wait for DOM to be ready
|
||||
setTimeout(() => {
|
||||
const handleSelector = Utils.isTouchScreenOrShowDesktopDragHandles()
|
||||
? '.js-list-handle'
|
||||
: '.js-list-header';
|
||||
const $lists = this.$('.js-list');
|
||||
|
||||
const $parent = $lists.parent();
|
||||
|
|
@ -1041,7 +1077,7 @@ BlazeComponent.extendComponent({
|
|||
items: '.js-list:not(.js-list-composer)',
|
||||
placeholder: 'list placeholder',
|
||||
distance: 7,
|
||||
handle: '.js-list-handle',
|
||||
handle: handleSelector,
|
||||
disabled: !Utils.canModifyBoard(),
|
||||
start(evt, ui) {
|
||||
ui.helper.css('z-index', 1000);
|
||||
|
|
@ -1054,6 +1090,15 @@ BlazeComponent.extendComponent({
|
|||
boardComponent.setIsDragging(false);
|
||||
}
|
||||
});
|
||||
// Reactively update handle when user toggles desktop drag handles
|
||||
this.autorun(() => {
|
||||
const newHandle = Utils.isTouchScreenOrShowDesktopDragHandles()
|
||||
? '.js-list-handle'
|
||||
: '.js-list-header';
|
||||
if ($parent.data('uiSortable') || $parent.data('sortable')) {
|
||||
try { $parent.sortable('option', 'handle', newHandle); } catch (e) {}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
}
|
||||
}, 100);
|
||||
|
|
|
|||
|
|
@ -79,13 +79,21 @@ Utils = {
|
|||
},
|
||||
|
||||
getMobileMode() {
|
||||
// Check localStorage first - user's explicit preference takes priority
|
||||
const stored = localStorage.getItem('wekan-mobile-mode');
|
||||
if (stored !== null) {
|
||||
return stored === 'true';
|
||||
}
|
||||
|
||||
// Then check user profile
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile && user.profile.mobileMode !== undefined) {
|
||||
return user.profile.mobileMode;
|
||||
}
|
||||
// For non-logged-in users, check localStorage
|
||||
const stored = localStorage.getItem('wekan-mobile-mode');
|
||||
return stored ? stored === 'true' : false;
|
||||
|
||||
// Default to mobile mode for iPhone/iPod
|
||||
const isIPhone = /iPhone|iPod/i.test(navigator.userAgent);
|
||||
return isIPhone;
|
||||
},
|
||||
|
||||
setMobileMode(enabled) {
|
||||
|
|
@ -93,13 +101,41 @@ Utils = {
|
|||
if (user) {
|
||||
// Update user profile
|
||||
user.setMobileMode(enabled);
|
||||
} else {
|
||||
// Store in localStorage for non-logged-in users
|
||||
localStorage.setItem('wekan-mobile-mode', enabled.toString());
|
||||
}
|
||||
// Always store in localStorage for persistence across sessions
|
||||
localStorage.setItem('wekan-mobile-mode', enabled.toString());
|
||||
Utils.applyMobileMode(enabled);
|
||||
// Trigger reactive updates for UI components
|
||||
Session.set('wekan-mobile-mode', enabled);
|
||||
// Re-apply zoom level to ensure proper rendering
|
||||
const zoomLevel = Utils.getZoomLevel();
|
||||
Utils.applyZoomLevel(zoomLevel);
|
||||
},
|
||||
|
||||
getCardZoom() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.profile && user.profile.cardZoom !== undefined) {
|
||||
return user.profile.cardZoom;
|
||||
}
|
||||
const stored = localStorage.getItem('wekan-card-zoom');
|
||||
return stored ? parseFloat(stored) : 1.0;
|
||||
},
|
||||
|
||||
setCardZoom(level) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
user.setCardZoom(level);
|
||||
}
|
||||
localStorage.setItem('wekan-card-zoom', level.toString());
|
||||
Utils.applyCardZoom(level);
|
||||
Session.set('wekan-card-zoom', level);
|
||||
},
|
||||
|
||||
applyCardZoom(level) {
|
||||
const cardDetails = document.querySelector('.card-details');
|
||||
if (cardDetails) {
|
||||
cardDetails.style.fontSize = `${level}em`;
|
||||
}
|
||||
},
|
||||
|
||||
applyZoomLevel(level) {
|
||||
|
|
@ -301,6 +337,85 @@ Utils = {
|
|||
}
|
||||
},
|
||||
|
||||
getListCollapseState(list) {
|
||||
if (!list) return false;
|
||||
const key = `collapsedList-${list._id}`;
|
||||
const sessionVal = Session.get(key);
|
||||
if (typeof sessionVal === 'boolean') {
|
||||
return sessionVal;
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
let stored = null;
|
||||
if (user && user.getCollapsedListFromStorage) {
|
||||
stored = user.getCollapsedListFromStorage(list.boardId, list._id);
|
||||
} else if (Users.getPublicCollapsedList) {
|
||||
stored = Users.getPublicCollapsedList(list.boardId, list._id);
|
||||
}
|
||||
|
||||
if (typeof stored === 'boolean') {
|
||||
Session.setDefault(key, stored);
|
||||
return stored;
|
||||
}
|
||||
|
||||
const fallback = typeof list.collapsed === 'boolean' ? list.collapsed : false;
|
||||
Session.setDefault(key, fallback);
|
||||
return fallback;
|
||||
},
|
||||
|
||||
setListCollapseState(list, collapsed) {
|
||||
if (!list) return;
|
||||
const key = `collapsedList-${list._id}`;
|
||||
Session.set(key, !!collapsed);
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
Meteor.call('setListCollapsedState', list.boardId, list._id, !!collapsed);
|
||||
} else if (Users.setPublicCollapsedList) {
|
||||
Users.setPublicCollapsedList(list.boardId, list._id, !!collapsed);
|
||||
}
|
||||
},
|
||||
|
||||
getSwimlaneCollapseState(swimlane) {
|
||||
if (!swimlane) return false;
|
||||
const key = `collapsedSwimlane-${swimlane._id}`;
|
||||
const sessionVal = Session.get(key);
|
||||
if (typeof sessionVal === 'boolean') {
|
||||
return sessionVal;
|
||||
}
|
||||
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
let stored = null;
|
||||
if (user && user.getCollapsedSwimlaneFromStorage) {
|
||||
stored = user.getCollapsedSwimlaneFromStorage(
|
||||
swimlane.boardId,
|
||||
swimlane._id,
|
||||
);
|
||||
} else if (Users.getPublicCollapsedSwimlane) {
|
||||
stored = Users.getPublicCollapsedSwimlane(swimlane.boardId, swimlane._id);
|
||||
}
|
||||
|
||||
if (typeof stored === 'boolean') {
|
||||
Session.setDefault(key, stored);
|
||||
return stored;
|
||||
}
|
||||
|
||||
const fallback = typeof swimlane.collapsed === 'boolean' ? swimlane.collapsed : false;
|
||||
Session.setDefault(key, fallback);
|
||||
return fallback;
|
||||
},
|
||||
|
||||
setSwimlaneCollapseState(swimlane, collapsed) {
|
||||
if (!swimlane) return;
|
||||
const key = `collapsedSwimlane-${swimlane._id}`;
|
||||
Session.set(key, !!collapsed);
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) {
|
||||
Meteor.call('setSwimlaneCollapsedState', swimlane.boardId, swimlane._id, !!collapsed);
|
||||
} else if (Users.setPublicCollapsedSwimlane) {
|
||||
Users.setPublicCollapsedSwimlane(swimlane.boardId, swimlane._id, !!collapsed);
|
||||
}
|
||||
},
|
||||
|
||||
myCardsSort() {
|
||||
let sort = window.localStorage.getItem('myCardsSort');
|
||||
|
||||
|
|
|
|||
|
|
@ -165,6 +165,16 @@ FlowRouter.route('/b/:boardId/:slug/:cardId', {
|
|||
Session.set('currentCard', params.cardId);
|
||||
Session.set('popupCardId', null);
|
||||
Session.set('popupCardBoardId', null);
|
||||
|
||||
// In desktop mode, add to openCards array to support multiple cards
|
||||
const isMobile = Utils.getMobileMode();
|
||||
if (!isMobile) {
|
||||
const openCards = Session.get('openCards') || [];
|
||||
if (!openCards.includes(params.cardId)) {
|
||||
openCards.push(params.cardId);
|
||||
Session.set('openCards', openCards);
|
||||
}
|
||||
}
|
||||
|
||||
Utils.manageCustomUI();
|
||||
Utils.manageMatomo();
|
||||
|
|
|
|||
|
|
@ -297,6 +297,23 @@ Lists.helpers({
|
|||
},
|
||||
|
||||
isCollapsed() {
|
||||
if (Meteor.isClient) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
// Logged-in users: prefer profile/cookie-backed state
|
||||
if (user && user.getCollapsedListFromStorage) {
|
||||
const stored = user.getCollapsedListFromStorage(this.boardId, this._id);
|
||||
if (typeof stored === 'boolean') {
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
// Public users: fallback to cookie if available
|
||||
if (!user && Users.getPublicCollapsedList) {
|
||||
const stored = Users.getPublicCollapsedList(this.boardId, this._id);
|
||||
if (typeof stored === 'boolean') {
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.collapsed === true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -246,6 +246,21 @@ Swimlanes.helpers({
|
|||
},
|
||||
|
||||
isCollapsed() {
|
||||
if (Meteor.isClient) {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user && user.getCollapsedSwimlaneFromStorage) {
|
||||
const stored = user.getCollapsedSwimlaneFromStorage(this.boardId, this._id);
|
||||
if (typeof stored === 'boolean') {
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
if (!user && Users.getPublicCollapsedSwimlane) {
|
||||
const stored = Users.getPublicCollapsedSwimlane(this.boardId, this._id);
|
||||
if (typeof stored === 'boolean') {
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.collapsed === true;
|
||||
},
|
||||
|
||||
|
|
|
|||
324
models/users.js
324
models/users.js
|
|
@ -11,6 +11,83 @@ const isSandstorm =
|
|||
Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
|
||||
Users = Meteor.users;
|
||||
|
||||
// Public-board collapse persistence helpers (cookie-based for non-logged-in users)
|
||||
if (Meteor.isClient) {
|
||||
const readCookieMap = name => {
|
||||
try {
|
||||
const stored = typeof document !== 'undefined' ? document.cookie : '';
|
||||
const cookies = stored.split(';').map(c => c.trim());
|
||||
let json = '{}';
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith(name + '=')) {
|
||||
json = decodeURIComponent(c.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return JSON.parse(json || '{}');
|
||||
} catch (e) {
|
||||
console.warn('Error parsing collapse cookie', name, e);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const writeCookieMap = (name, data) => {
|
||||
try {
|
||||
const serialized = encodeURIComponent(JSON.stringify(data || {}));
|
||||
const maxAge = 60 * 60 * 24 * 365; // 1 year
|
||||
document.cookie = `${name}=${serialized}; path=/; max-age=${maxAge}`;
|
||||
} catch (e) {
|
||||
console.warn('Error writing collapse cookie', name, e);
|
||||
}
|
||||
};
|
||||
|
||||
Users.getPublicCollapsedList = (boardId, listId) => {
|
||||
if (!boardId || !listId) return null;
|
||||
const data = readCookieMap('wekan-collapsed-lists');
|
||||
if (data[boardId] && typeof data[boardId][listId] === 'boolean') {
|
||||
return data[boardId][listId];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Users.setPublicCollapsedList = (boardId, listId, collapsed) => {
|
||||
if (!boardId || !listId) return false;
|
||||
const data = readCookieMap('wekan-collapsed-lists');
|
||||
if (!data[boardId]) data[boardId] = {};
|
||||
data[boardId][listId] = !!collapsed;
|
||||
writeCookieMap('wekan-collapsed-lists', data);
|
||||
return true;
|
||||
};
|
||||
|
||||
Users.getPublicCollapsedSwimlane = (boardId, swimlaneId) => {
|
||||
if (!boardId || !swimlaneId) return null;
|
||||
const data = readCookieMap('wekan-collapsed-swimlanes');
|
||||
if (data[boardId] && typeof data[boardId][swimlaneId] === 'boolean') {
|
||||
return data[boardId][swimlaneId];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
Users.setPublicCollapsedSwimlane = (boardId, swimlaneId, collapsed) => {
|
||||
if (!boardId || !swimlaneId) return false;
|
||||
const data = readCookieMap('wekan-collapsed-swimlanes');
|
||||
if (!data[boardId]) data[boardId] = {};
|
||||
data[boardId][swimlaneId] = !!collapsed;
|
||||
writeCookieMap('wekan-collapsed-swimlanes', data);
|
||||
return true;
|
||||
};
|
||||
|
||||
Users.getPublicCardCollapsed = () => {
|
||||
const data = readCookieMap('wekan-card-collapsed');
|
||||
return typeof data.state === 'boolean' ? data.state : null;
|
||||
};
|
||||
|
||||
Users.setPublicCardCollapsed = collapsed => {
|
||||
writeCookieMap('wekan-card-collapsed', { state: !!collapsed });
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
const allowedSortValues = [
|
||||
'-modifiedAt',
|
||||
'modifiedAt',
|
||||
|
|
@ -187,6 +264,13 @@ Users.attachSchema(
|
|||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
'profile.cardCollapsed': {
|
||||
/**
|
||||
* has user collapsed the card details?
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
'profile.customFieldsGrid': {
|
||||
/**
|
||||
* has user at card Custom Fields have Grid (false) or one per row (true) layout?
|
||||
|
|
@ -476,6 +560,24 @@ Users.attachSchema(
|
|||
defaultValue: {},
|
||||
blackbox: true,
|
||||
},
|
||||
'profile.collapsedLists': {
|
||||
/**
|
||||
* Per-user collapsed state for lists.
|
||||
* profile[boardId][listId] = true|false
|
||||
*/
|
||||
type: Object,
|
||||
defaultValue: {},
|
||||
blackbox: true,
|
||||
},
|
||||
'profile.collapsedSwimlanes': {
|
||||
/**
|
||||
* Per-user collapsed state for swimlanes.
|
||||
* profile[boardId][swimlaneId] = true|false
|
||||
*/
|
||||
type: Object,
|
||||
defaultValue: {},
|
||||
blackbox: true,
|
||||
},
|
||||
'profile.keyboardShortcuts': {
|
||||
/**
|
||||
* User-specified state of keyboard shortcut activation.
|
||||
|
|
@ -522,6 +624,15 @@ Users.attachSchema(
|
|||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
'profile.cardZoom': {
|
||||
/**
|
||||
* User-specified zoom level for card details (1.0 = 100%, 1.5 = 150%, etc.)
|
||||
*/
|
||||
type: Number,
|
||||
defaultValue: 1.0,
|
||||
min: 0.5,
|
||||
max: 3.0,
|
||||
},
|
||||
services: {
|
||||
/**
|
||||
* services field of the user
|
||||
|
|
@ -602,7 +713,7 @@ Users.attachSchema(
|
|||
);
|
||||
|
||||
// Security helpers for user updates
|
||||
export const USER_UPDATE_ALLOWED_EXACT = ['username'];
|
||||
export const USER_UPDATE_ALLOWED_EXACT = ['username', 'profile'];
|
||||
export const USER_UPDATE_ALLOWED_PREFIXES = ['profile.'];
|
||||
export const USER_UPDATE_FORBIDDEN_PREFIXES = [
|
||||
'services',
|
||||
|
|
@ -1311,6 +1422,135 @@ Users.helpers({
|
|||
return false;
|
||||
}
|
||||
},
|
||||
// Per-user collapsed state helpers for lists/swimlanes
|
||||
getCollapsedList(boardId, listId) {
|
||||
const { collapsedLists = {} } = this.profile || {};
|
||||
if (collapsedLists[boardId] && typeof collapsedLists[boardId][listId] === 'boolean') {
|
||||
return collapsedLists[boardId][listId];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getCollapsedSwimlane(boardId, swimlaneId) {
|
||||
const { collapsedSwimlanes = {} } = this.profile || {};
|
||||
if (collapsedSwimlanes[boardId] && typeof collapsedSwimlanes[boardId][swimlaneId] === 'boolean') {
|
||||
return collapsedSwimlanes[boardId][swimlaneId];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
setCollapsedListToStorage(boardId, listId, collapsed) {
|
||||
// Logged-in users: save to profile
|
||||
if (this._id) {
|
||||
return this.setCollapsedList(boardId, listId, collapsed);
|
||||
}
|
||||
// Public users: save to cookie
|
||||
try {
|
||||
const name = 'wekan-collapsed-lists';
|
||||
const stored = (typeof document !== 'undefined') ? document.cookie : '';
|
||||
const cookies = stored.split(';').map(c => c.trim());
|
||||
let json = '{}';
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith(name + '=')) {
|
||||
json = decodeURIComponent(c.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let data = {};
|
||||
try { data = JSON.parse(json || '{}'); } catch (e) { data = {}; }
|
||||
if (!data[boardId]) data[boardId] = {};
|
||||
data[boardId][listId] = !!collapsed;
|
||||
const serialized = encodeURIComponent(JSON.stringify(data));
|
||||
const maxAge = 60 * 60 * 24 * 365; // 1 year
|
||||
document.cookie = `${name}=${serialized}; path=/; max-age=${maxAge}`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('Error saving collapsed list to cookie:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getCollapsedListFromStorage(boardId, listId) {
|
||||
// Logged-in users: read from profile
|
||||
if (this._id) {
|
||||
const v = this.getCollapsedList(boardId, listId);
|
||||
return v;
|
||||
}
|
||||
// Public users: read from cookie
|
||||
try {
|
||||
const name = 'wekan-collapsed-lists';
|
||||
const stored = (typeof document !== 'undefined') ? document.cookie : '';
|
||||
const cookies = stored.split(';').map(c => c.trim());
|
||||
let json = '{}';
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith(name + '=')) {
|
||||
json = decodeURIComponent(c.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const data = JSON.parse(json || '{}');
|
||||
if (data[boardId] && typeof data[boardId][listId] === 'boolean') {
|
||||
return data[boardId][listId];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error reading collapsed list from cookie:', e);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
setCollapsedSwimlaneToStorage(boardId, swimlaneId, collapsed) {
|
||||
// Logged-in users: save to profile
|
||||
if (this._id) {
|
||||
return this.setCollapsedSwimlane(boardId, swimlaneId, collapsed);
|
||||
}
|
||||
// Public users: save to cookie
|
||||
try {
|
||||
const name = 'wekan-collapsed-swimlanes';
|
||||
const stored = (typeof document !== 'undefined') ? document.cookie : '';
|
||||
const cookies = stored.split(';').map(c => c.trim());
|
||||
let json = '{}';
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith(name + '=')) {
|
||||
json = decodeURIComponent(c.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let data = {};
|
||||
try { data = JSON.parse(json || '{}'); } catch (e) { data = {}; }
|
||||
if (!data[boardId]) data[boardId] = {};
|
||||
data[boardId][swimlaneId] = !!collapsed;
|
||||
const serialized = encodeURIComponent(JSON.stringify(data));
|
||||
const maxAge = 60 * 60 * 24 * 365; // 1 year
|
||||
document.cookie = `${name}=${serialized}; path=/; max-age=${maxAge}`;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn('Error saving collapsed swimlane to cookie:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getCollapsedSwimlaneFromStorage(boardId, swimlaneId) {
|
||||
// Logged-in users: read from profile
|
||||
if (this._id) {
|
||||
const v = this.getCollapsedSwimlane(boardId, swimlaneId);
|
||||
return v;
|
||||
}
|
||||
// Public users: read from cookie
|
||||
try {
|
||||
const name = 'wekan-collapsed-swimlanes';
|
||||
const stored = (typeof document !== 'undefined') ? document.cookie : '';
|
||||
const cookies = stored.split(';').map(c => c.trim());
|
||||
let json = '{}';
|
||||
for (const c of cookies) {
|
||||
if (c.startsWith(name + '=')) {
|
||||
json = decodeURIComponent(c.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const data = JSON.parse(json || '{}');
|
||||
if (data[boardId] && typeof data[boardId][swimlaneId] === 'boolean') {
|
||||
return data[boardId][swimlaneId];
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error reading collapsed swimlane from cookie:', e);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
Users.mutations({
|
||||
|
|
@ -1485,6 +1725,14 @@ Users.mutations({
|
|||
};
|
||||
},
|
||||
|
||||
toggleCardCollapsed(value = false) {
|
||||
return {
|
||||
$set: {
|
||||
'profile.cardCollapsed': !value,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
toggleLabelText(value = false) {
|
||||
return {
|
||||
$set: {
|
||||
|
|
@ -1621,6 +1869,26 @@ Users.mutations({
|
|||
},
|
||||
};
|
||||
},
|
||||
setCollapsedList(boardId, listId, collapsed) {
|
||||
const current = (this.profile && this.profile.collapsedLists) || {};
|
||||
if (!current[boardId]) current[boardId] = {};
|
||||
current[boardId][listId] = !!collapsed;
|
||||
return {
|
||||
$set: {
|
||||
'profile.collapsedLists': current,
|
||||
},
|
||||
};
|
||||
},
|
||||
setCollapsedSwimlane(boardId, swimlaneId, collapsed) {
|
||||
const current = (this.profile && this.profile.collapsedSwimlanes) || {};
|
||||
if (!current[boardId]) current[boardId] = {};
|
||||
current[boardId][swimlaneId] = !!collapsed;
|
||||
return {
|
||||
$set: {
|
||||
'profile.collapsedSwimlanes': current,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
setZoomLevel(level) {
|
||||
return {
|
||||
|
|
@ -1637,6 +1905,14 @@ Users.mutations({
|
|||
},
|
||||
};
|
||||
},
|
||||
|
||||
setCardZoom(level) {
|
||||
return {
|
||||
$set: {
|
||||
'profile.cardZoom': level,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
|
|
@ -1809,6 +2085,11 @@ Meteor.methods({
|
|||
const user = ReactiveCache.getCurrentUser();
|
||||
user.toggleCardMaximized(user.hasCardMaximized());
|
||||
},
|
||||
setCardCollapsed(value) {
|
||||
check(value, Boolean);
|
||||
if (!this.userId) throw new Meteor.Error('not-logged-in');
|
||||
Users.update(this.userId, { $set: { 'profile.cardCollapsed': value } });
|
||||
},
|
||||
toggleMinicardLabelText() {
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
user.toggleLabelText(user.hasHiddenMinicardLabelText());
|
||||
|
|
@ -1838,6 +2119,26 @@ Meteor.methods({
|
|||
user.setListWidth(boardId, listId, width);
|
||||
user.setListConstraint(boardId, listId, constraint);
|
||||
},
|
||||
setListCollapsedState(boardId, listId, collapsed) {
|
||||
check(boardId, String);
|
||||
check(listId, String);
|
||||
check(collapsed, Boolean);
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-logged-in', 'User must be logged in');
|
||||
}
|
||||
const user = Users.findOne(this.userId);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('user-not-found', 'User not found');
|
||||
}
|
||||
const current = (user.profile && user.profile.collapsedLists) || {};
|
||||
if (!current[boardId]) current[boardId] = {};
|
||||
current[boardId][listId] = !!collapsed;
|
||||
Users.update(this.userId, {
|
||||
$set: {
|
||||
'profile.collapsedLists': current,
|
||||
},
|
||||
});
|
||||
},
|
||||
applySwimlaneHeight(boardId, swimlaneId, height) {
|
||||
check(boardId, String);
|
||||
check(swimlaneId, String);
|
||||
|
|
@ -1846,6 +2147,27 @@ Meteor.methods({
|
|||
user.setSwimlaneHeight(boardId, swimlaneId, height);
|
||||
},
|
||||
|
||||
setSwimlaneCollapsedState(boardId, swimlaneId, collapsed) {
|
||||
check(boardId, String);
|
||||
check(swimlaneId, String);
|
||||
check(collapsed, Boolean);
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-logged-in', 'User must be logged in');
|
||||
}
|
||||
const user = Users.findOne(this.userId);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('user-not-found', 'User not found');
|
||||
}
|
||||
const current = (user.profile && user.profile.collapsedSwimlanes) || {};
|
||||
if (!current[boardId]) current[boardId] = {};
|
||||
current[boardId][swimlaneId] = !!collapsed;
|
||||
Users.update(this.userId, {
|
||||
$set: {
|
||||
'profile.collapsedSwimlanes': current,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
applySwimlaneHeightToStorage(boardId, swimlaneId, height) {
|
||||
check(boardId, String);
|
||||
check(swimlaneId, String);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue