Browse Source

Number of cards per list and sum of custom number field in list head.

Thanks to xet7 !

Fixes #3796
Lauri Ojansivu 15 hours ago
parent
commit
e569c2957e
29 changed files with 239 additions and 142 deletions
  1. 5 8
      client/components/activities/comments.css
  2. 3 3
      client/components/activities/comments.jade
  3. 3 3
      client/components/boards/boardArchive.jade
  4. 1 1
      client/components/boards/miniboard.jade
  5. 2 2
      client/components/cards/cardDetails.css
  6. 6 6
      client/components/cards/cardDetails.jade
  7. 10 10
      client/components/cards/checklists.css
  8. 2 2
      client/components/cards/checklists.jade
  9. 6 2
      client/components/cards/labels.css
  10. 1 1
      client/components/cards/labels.jade
  11. 4 19
      client/components/cards/minicard.css
  12. 1 1
      client/components/cards/minicard.jade
  13. 3 5
      client/components/common/originalPosition.html
  14. 2 2
      client/components/import/import.jade
  15. 18 0
      client/components/lists/list.css
  16. 58 3
      client/components/lists/listBody.js
  17. 6 0
      client/components/lists/listHeader.jade
  18. 42 1
      client/components/lists/listHeader.js
  19. 1 1
      client/components/lists/minilist.jade
  20. 3 3
      client/components/main/layouts.css
  21. 4 4
      client/components/notifications/notification.css
  22. 1 1
      client/components/notifications/notification.jade
  23. 14 14
      client/components/notifications/notificationIcon.jade
  24. 0 3
      client/components/notifications/notificationsDrawer.css
  25. 2 2
      client/components/notifications/notificationsDrawer.jade
  26. 11 11
      client/components/sidebar/sidebarFilters.jade
  27. 2 11
      client/components/swimlanes/swimlaneHeader.jade
  28. 24 19
      client/components/swimlanes/swimlanes.css
  29. 4 4
      client/components/users/userAvatar.css

+ 5 - 8
client/components/activities/comments.css

@@ -108,15 +108,12 @@
   text-decoration: none;
   text-decoration: none;
   height: 24px;
   height: 24px;
 }
 }
-.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-smile-o {
-  font-size: 17px;
+.comments .comment .comment-desc .reactions .open-comment-reaction-popup span {
+  display: inline-block;
+  font-size: clamp(14px, 2vw, 18px);
   font-weight: 500;
   font-weight: 500;
-  margin-left: 2px;
-}
-.comments .comment .comment-desc .reactions .open-comment-reaction-popup i.fa.fa-plus {
-  font-size: 8px;
-  margin-top: -7px;
-  margin-left: 1px;
+  line-height: 1;
+  margin-left: 4px;
 }
 }
 .comments .comment .comment-desc .reactions .reaction {
 .comments .comment .comment-desc .reactions .reaction {
   cursor: pointer;
   cursor: pointer;

+ 3 - 3
client/components/activities/comments.jade

@@ -25,7 +25,7 @@ template(name="comment")
           = text
           = text
         .edit-controls
         .edit-controls
           button.primary(type="submit") {{_ 'edit'}}
           button.primary(type="submit") {{_ 'edit'}}
-          .fa.fa-times-thin.js-close-inlined-form
+          a.js-close-inlined-form(title="{{_ 'close' }}") ❌
       else
       else
         .comment-text
         .comment-text
           +viewer
           +viewer
@@ -55,8 +55,8 @@ template(name="commentReactions")
         span.reaction-count #{reaction.userIds.length}
         span.reaction-count #{reaction.userIds.length}
     if (currentUser.isBoardMember)
     if (currentUser.isBoardMember)
       a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
       a.open-comment-reaction-popup(title="{{_ 'addReactionPopup-title'}}")
-        i.fa.fa-smile-o
-        i.fa.fa-plus
+        span(title="{{_ 'reaction' }}") 😀
+        span(title="{{_ 'add' }}") ➕
 
 
 template(name="addReactionPopup")
 template(name="addReactionPopup")
   .reactions-popup
   .reactions-popup

+ 3 - 3
client/components/boards/boardArchive.jade

@@ -1,6 +1,6 @@
 template(name="archivedBoards")
 template(name="archivedBoards")
   h2
   h2
-    i.fa.fa-archive
+    span(title="{{_ 'archived-boards'}}") 📦
     | {{_ 'archived-boards'}}
     | {{_ 'archived-boards'}}
 
 
   ul.archived-lists
   ul.archived-lists
@@ -8,10 +8,10 @@ template(name="archivedBoards")
       li.archived-lists-item
       li.archived-lists-item
         div.board-header-btns
         div.board-header-btns
           button.board-header-btn.js-delete-board
           button.board-header-btn.js-delete-board
-            i.fa.fa-trash-o
+            | 🗑️
             | {{_ 'delete-board'}}
             | {{_ 'delete-board'}}
           button.board-header-btn.js-restore-board
           button.board-header-btn.js-restore-board
-            i.fa.fa-undo
+            | ↩️
             | {{_ 'restore-board'}}
             | {{_ 'restore-board'}}
           = title
           = title
         span {{ moment archivedAt 'LLL' }}
         span {{ moment archivedAt 'LLL' }}

+ 1 - 1
client/components/boards/miniboard.jade

@@ -3,6 +3,6 @@ template(name="miniboard")
     class="minicard-{{colorClass}}")
     class="minicard-{{colorClass}}")
     .minicard-title
     .minicard-title
       .handle
       .handle
-        .fa.fa-arrows
+        span.drag-handle(title="{{_ 'dragBoard'}}") ↕️
       +viewer
       +viewer
         = title
         = title

+ 2 - 2
client/components/cards/cardDetails.css

@@ -31,8 +31,8 @@
   display: block;
   display: block;
   position: relative;
   position: relative;
   float: left;
   float: left;
-  height: 30px;
-  width: 30px;
+  height: clamp(24px, 3.5vw, 36px);
+  width: clamp(24px, 3.5vw, 36px);
   margin: .3vh;
   margin: .3vh;
   cursor: pointer;
   cursor: pointer;
   user-select: none;
   user-select: none;

+ 6 - 6
client/components/cards/cardDetails.jade

@@ -26,10 +26,10 @@ template(name="cardDetails")
               | ☰
               | ☰
             a.card-copy-button.js-copy-link(
             a.card-copy-button.js-copy-link(
               id="cardURL_copy"
               id="cardURL_copy"
-              class="fa-link"
               title="{{_ 'copy-card-link-to-clipboard'}}"
               title="{{_ 'copy-card-link-to-clipboard'}}"
               href="{{ originRelativeUrl }}"
               href="{{ originRelativeUrl }}"
             )
             )
+              | 🔗
             span.copied-tooltip {{_ 'copied'}}
             span.copied-tooltip {{_ 'copied'}}
         else
         else
           unless isPopup
           unless isPopup
@@ -40,10 +40,10 @@ template(name="cardDetails")
               | ☰
               | ☰
             a.card-copy-mobile-button.js-copy-link(
             a.card-copy-mobile-button.js-copy-link(
               id="cardURL_copy"
               id="cardURL_copy"
-              class="fa-link"
               title="{{_ 'copy-card-link-to-clipboard'}}"
               title="{{_ 'copy-card-link-to-clipboard'}}"
               href="{{ originRelativeUrl }}"
               href="{{ originRelativeUrl }}"
             )
             )
+              | 🔗
             span.copied-tooltip {{_ 'copied'}}
             span.copied-tooltip {{_ 'copied'}}
         h2.card-details-title.js-card-title(
         h2.card-details-title.js-card-title(
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
           class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
@@ -304,7 +304,7 @@ template(name="cardDetails")
               hr
               hr
             .card-details-item.card-details-item-customfield
             .card-details-item.card-details-item-customfield
               h3.card-details-item-title
               h3.card-details-item-title
-                | 📋-alt
+                | 📋
                 = definition.name
                 = definition.name
               +cardCustomField
               +cardCustomField
 
 
@@ -678,7 +678,7 @@ template(name="cardDetailsActionsPopup")
           | 👁️
           | 👁️
           |  {{_ 'unwatch'}}
           |  {{_ 'unwatch'}}
         else
         else
-          | 👁️-slash
+          | 👁️
           |  {{_ 'watch'}}
           |  {{_ 'watch'}}
   hr
   hr
   if canModifyCard
   if canModifyCard
@@ -698,7 +698,7 @@ template(name="cardDetailsActionsPopup")
         if currentUser.isBoardAdmin
         if currentUser.isBoardAdmin
           li
           li
             a.js-custom-fields
             a.js-custom-fields
-              | 📋-alt
+              | 📋
               | {{_ 'card-edit-custom-fields'}}
               | {{_ 'card-edit-custom-fields'}}
         //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
         //li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
         //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
         //li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
@@ -718,7 +718,7 @@ template(name="cardDetailsActionsPopup")
               | 👁️
               | 👁️
               | {{_ 'hide-list-on-minicard'}}
               | {{_ 'hide-list-on-minicard'}}
             else
             else
-              | 👁️-slash
+              | 👁️
               | {{_ 'show-list-on-minicard'}}
               | {{_ 'show-list-on-minicard'}}
   hr
   hr
   ul.pop-over-list
   ul.pop-over-list

+ 10 - 10
client/components/cards/checklists.css

@@ -67,14 +67,14 @@ textarea.js-edit-checklist-item {
 .checklist-title .checklist-stat.is-finished {
 .checklist-title .checklist-stat.is-finished {
   color: #3cb500;
   color: #3cb500;
 }
 }
-.checklist-title span.fa.checklist-handle {
+.checklist-title span.checklist-handle {
   padding-right: 20px;
   padding-right: 20px;
   padding-top: 3px;
   padding-top: 3px;
   float: left;
   float: left;
-}
-.checklist-title span.fa.checklist-handle.fa-arrows::before {
-  content: "↕️" !important;
-  font-family: inherit !important;
+  display: inline-block;
+  width: 1.2em;
+  text-align: center;
+  color: #999;
 }
 }
 #card-details-overlay {
 #card-details-overlay {
   top: 0;
   top: 0;
@@ -148,13 +148,13 @@ textarea.js-edit-checklist-item {
   word-wrap: break-word;
   word-wrap: break-word;
   max-width: 420px;
   max-width: 420px;
 }
 }
-.checklist-item span.fa.checklistitem-handle {
+.checklist-item span.checklistitem-handle {
   padding-top: 2px;
   padding-top: 2px;
   padding-right: 10px;
   padding-right: 10px;
-}
-.checklist-item span.fa.checklistitem-handle.fa-arrows::before {
-  content: "↕️" !important;
-  font-family: inherit !important;
+  display: inline-block;
+  width: 1.2em;
+  text-align: center;
+  color: #999;
 }
 }
 .js-delete-checklist-item,
 .js-delete-checklist-item,
 .js-convert-checklist-item-to-card {
 .js-convert-checklist-item-to-card {

+ 2 - 2
client/components/cards/checklists.jade

@@ -43,7 +43,7 @@ template(name="checklistDetail")
         if canModifyCard
         if canModifyCard
           h4.title.js-open-inlined-form.is-editable
           h4.title.js-open-inlined-form.is-editable
             if isTouchScreenOrShowDesktopDragHandles
             if isTouchScreenOrShowDesktopDragHandles
-              span.fa.checklist-handle(class="fa-arrows" title="{{_ 'dragChecklist'}}")
+              span.checklist-handle(title="{{_ 'dragChecklist'}}") ↕️
             +viewer
             +viewer
               = checklist.title
               = checklist.title
         else
         else
@@ -127,7 +127,7 @@ template(name='checklistItemDetail')
     if canModifyCard
     if canModifyCard
       .check-box-container
       .check-box-container
         .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
         .check-box.materialCheckBox(class="{{#if item.isFinished }}is-checked{{/if}}")
-      span.fa.checklistitem-handle(class="fa-arrows" title="{{_ 'dragChecklistItem'}}")
+      span.checklistitem-handle(title="{{_ 'dragChecklistItem'}}") ↕️
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
       .item-title.js-open-inlined-form.is-editable(class="{{#if item.isFinished }}is-checked{{/if}}")
         +viewer
         +viewer
           = item.title
           = item.title

+ 6 - 2
client/components/cards/labels.css

@@ -223,9 +223,13 @@
 .card-label-edit-button:hover {
 .card-label-edit-button:hover {
   background: #dbdbdb;
   background: #dbdbdb;
 }
 }
-ul.edit-labels-pop-over span.fa.label-handle {
+ul.edit-labels-pop-over span.label-handle {
   padding-right: 10px;
   padding-right: 10px;
+  display: inline-block;
+  width: 1.2em;
+  text-align: center;
+  color: #999;
 }
 }
-ul.edit-labels-pop-over span.fa.label-handle + .card-label {
+ul.edit-labels-pop-over span.label-handle + .card-label {
   max-width: 180px;
   max-width: 180px;
 }
 }

+ 1 - 1
client/components/cards/labels.jade

@@ -31,7 +31,7 @@ template(name="cardLabelsPopup")
         a.card-label-edit-button.js-edit-label
         a.card-label-edit-button.js-edit-label
           | ✏️
           | ✏️
         if isTouchScreenOrShowDesktopDragHandles
         if isTouchScreenOrShowDesktopDragHandles
-          span.fa.label-handle(class="fa-arrows" title="{{_ 'dragLabel'}}")
+          span.label-handle(title="{{_ 'dragLabel'}}") ↕️
         span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
         span.card-label.card-label-selectable.js-select-label.card-label-wrapper(class="card-label-{{color}}"
           class="{{# if isLabelSelected ../_id }}active{{/if}}")
           class="{{# if isLabelSelected ../_id }}active{{/if}}")
             +viewer
             +viewer

+ 4 - 19
client/components/cards/minicard.css

@@ -142,9 +142,12 @@
     display: block;
     display: block;
   }
   }
 }
 }
-.minicard .handle .fa-arrows {
+.minicard .handle .drag-handle {
   font-size: clamp(16px, 3vw, 20px);
   font-size: clamp(16px, 3vw, 20px);
   color: #ccc;
   color: #ccc;
+  display: inline-block;
+  width: 1.4em;
+  text-align: center;
 }
 }
 .minicard .minicard-title .card-number {
 .minicard .minicard-title .card-number {
   color: #b3b3b3;
   color: #b3b3b3;
@@ -297,19 +300,6 @@
   background-color: #1976d2 !important;
   background-color: #1976d2 !important;
 }
 }
 
 
-/* Font Awesome icons in minicard dates */
-.minicard .card-date i.fa {
-  margin-right: 0.3vw;
-  font-size: 0.9em;
-  vertical-align: middle;
-}
-
-/* Font Awesome icons in minicard spent time */
-.minicard .card-time i.fa {
-  margin-right: 0.3vw;
-  font-size: 0.9em;
-  vertical-align: middle;
-}
 .minicard .badges {
 .minicard .badges {
   float: left;
   float: left;
   margin-top: 1vh;
   margin-top: 1vh;
@@ -740,8 +730,3 @@
   align-items: center;
   align-items: center;
   gap: 0.3vw;
   gap: 0.3vw;
 }
 }
-
-.minicard-list-name i.fa {
-  font-size: 0.8em;
-  opacity: 0.7;
-}

+ 1 - 1
client/components/cards/minicard.jade

@@ -251,6 +251,6 @@ template(name="minicardDetailsActionsPopup")
           | 👁️
           | 👁️
           |  {{_ 'unwatch'}}
           |  {{_ 'unwatch'}}
         else
         else
-          | 👁️-slash
+          | 👁️
           |  {{_ 'watch'}}
           |  {{_ 'watch'}}
 
 

+ 3 - 5
client/components/common/originalPosition.html

@@ -2,19 +2,17 @@
   <div class="original-position-info">
   <div class="original-position-info">
     {{#if isLoading}}
     {{#if isLoading}}
       <div class="original-position-loading">
       <div class="original-position-loading">
-        <i class="fa fa-spinner fa-spin"></i> Loading original position...
+         Loading original position...
       </div>
       </div>
     {{else if showOriginalPosition}}
     {{else if showOriginalPosition}}
       <div class="original-position-details">
       <div class="original-position-details">
         {{#if hasMovedFromOriginal}}
         {{#if hasMovedFromOriginal}}
           <div class="original-position-moved">
           <div class="original-position-moved">
-            <i class="fa fa-info-circle"></i>
-            <span class="original-position-text">{{getOriginalPositionDescription}}</span>
+            <span class="original-position-text">ℹ️ {{getOriginalPositionDescription}}</span>
           </div>
           </div>
         {{else}}
         {{else}}
           <div class="original-position-unchanged">
           <div class="original-position-unchanged">
-            <i class="fa fa-check-circle"></i>
-            <span class="original-position-text">In original position</span>
+            <span class="original-position-text">✅ In original position</span>
           </div>
           </div>
         {{/if}}
         {{/if}}
         
         

+ 2 - 2
client/components/import/import.jade

@@ -1,7 +1,7 @@
 template(name="importHeaderBar")
 template(name="importHeaderBar")
   h1
   h1
     a.back-btn(href="{{pathFor 'home'}}")
     a.back-btn(href="{{pathFor 'home'}}")
-      i.fa.fa-chevron-left
+      | ⬅️
     | {{_ title}}
     | {{_ title}}
 
 
 template(name="import")
 template(name="import")
@@ -36,7 +36,7 @@ template(name="importMapMembers")
                 +userAvatar(userId=wekanId)
                 +userAvatar(userId=wekanId)
               else
               else
                 a.member.add-member
                 a.member.add-member
-                  i.fa.fa-plus
+                  | ➕
         //-
         //-
           Due to the way the flewbox layout is working, we need to set some
           Due to the way the flewbox layout is working, we need to set some
           invisible items so that the last row items have a consistent width.
           invisible items so that the last row items have a consistent width.

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

@@ -368,6 +368,18 @@ body.list-resizing-active * {
   text-overflow: ellipsis;
   text-overflow: ellipsis;
   word-wrap: break-word;
   word-wrap: break-word;
 }
 }
+/* Sum badge shown before list title */
+.list-header .list-sum-badge {
+  display: inline-block;
+  margin-right: 8px;
+  padding: 0;
+  border-radius: 0;
+  background: transparent;
+  color: #8c8c8c;
+  font-weight: bold;
+  font-size: 12px;
+  vertical-align: middle;
+}
 .list-rotated {
 .list-rotated {
   width: 1.3vw;
   width: 1.3vw;
   height: 35vh;
   height: 35vh;
@@ -750,6 +762,9 @@ body.list-resizing-active * {
   grid-row: 2;
   grid-row: 2;
   grid-column: 2;
   grid-column: 2;
   align-self: start;
   align-self: start;
+  text-align: left;
+  padding-left: 0;
+  margin-left: 0;
   font-size: 16px !important;
   font-size: 16px !important;
   line-height: 1.2;
   line-height: 1.2;
 }
 }
@@ -964,6 +979,9 @@ body.list-resizing-active * {
     grid-row: 2 !important;
     grid-row: 2 !important;
     grid-column: 2 !important;
     grid-column: 2 !important;
     align-self: start !important;
     align-self: start !important;
+    text-align: left !important;
+    padding-left: 0 !important;
+    margin-left: 0 !important;
     font-size: 16px !important;
     font-size: 16px !important;
     line-height: 1.2 !important;
     line-height: 1.2 !important;
   }
   }

+ 58 - 3
client/components/lists/listBody.js

@@ -16,11 +16,50 @@ BlazeComponent.extendComponent({
   },
   },
 
 
   customFieldsSum() {
   customFieldsSum() {
-    const ret = ReactiveCache.getCustomFields({
-      boardIds: { $in: [Session.get('currentBoard')] },
+    const list = Template.currentData();
+    if (!list) return [];
+    const boardId = Session.get('currentBoard');
+    const fields = ReactiveCache.getCustomFields({
+      boardIds: { $in: [boardId] },
       showSumAtTopOfList: true,
       showSumAtTopOfList: true,
     });
     });
-    return ret;
+
+    if (!fields || !fields.length) return [];
+
+    const cards = ReactiveCache.getCards({
+      listId: list._id,
+      archived: false,
+    });
+
+    const result = fields.map(field => {
+      let sum = 0;
+      if (cards && cards.length) {
+        cards.forEach(card => {
+          const cfs = (card.customFields || []);
+          const cf = cfs.find(f => f && f._id === field._id);
+          if (!cf || cf.value === null || cf.value === undefined) return;
+          let v = cf.value;
+          if (typeof v === 'string') {
+            // try to parse string numbers, accept comma decimal
+            const parsed = parseFloat(v.replace(',', '.'));
+            if (isNaN(parsed)) return;
+            v = parsed;
+          }
+          if (typeof v === 'number' && isFinite(v)) {
+            sum += v;
+          }
+        });
+      }
+      return {
+        _id: field._id,
+        name: field.name,
+        type: field.type,
+        settings: field.settings || {},
+        value: sum,
+      };
+    });
+
+    return result;
   },
   },
 
 
   openForm(options) {
   openForm(options) {
@@ -254,6 +293,22 @@ BlazeComponent.extendComponent({
   },
   },
 }).register('listBody');
 }).register('listBody');
 
 
+// Helpers for listBody template context
+Template.listBody.helpers({
+  formattedCurrencyCustomFieldValue(val) {
+    // `this` is the custom field sum object from customFieldsSum each-iteration
+    const field = this || {};
+    const code = (field.settings && field.settings.currencyCode) || 'USD';
+    try {
+      const n = typeof val === 'number' ? val : parseFloat(val);
+      if (!isFinite(n)) return val;
+      return new Intl.NumberFormat(undefined, { style: 'currency', currency: code }).format(n);
+    } catch (e) {
+      return `${code} ${val}`;
+    }
+  },
+});
+
 function toggleValueInReactiveArray(reactiveValue, value) {
 function toggleValueInReactiveArray(reactiveValue, value) {
   const array = reactiveValue.get();
   const array = reactiveValue.get();
   const valueIndex = array.indexOf(value);
   const valueIndex = array.indexOf(value);

+ 6 - 0
client/components/lists/listHeader.jade

@@ -26,6 +26,9 @@ template(name="listHeader")
            |/#{wipLimit.value})
            |/#{wipLimit.value})
         if showCardsCountForList cards.length
         if showCardsCountForList cards.length
           span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
           span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
+          if hasNumberFieldsSum
+            | &nbsp;
+            span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
       else
       else
         if collapsed
         if collapsed
           a.js-collapse(title="{{_ 'uncollapse'}}")
           a.js-collapse(title="{{_ 'uncollapse'}}")
@@ -44,6 +47,9 @@ template(name="listHeader")
         unless collapsed
         unless collapsed
           if showCardsCountForList cards.length
           if showCardsCountForList cards.length
             span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
             span.cardCount {{cardsCount}} {{cardsCountForListIsOne cards.length}}
+            if hasNumberFieldsSum
+              | &nbsp;
+              span.list-sum-badge(title="{{_ 'sum-of-number-fields'}}") ∑ {{numberFieldsSum}}
       if isMiniScreen
       if isMiniScreen
         if currentList
         if currentList
           if isWatching
           if isWatching

+ 42 - 1
client/components/lists/listHeader.js

@@ -142,7 +142,48 @@ BlazeComponent.extendComponent({
 Template.listHeader.helpers({
 Template.listHeader.helpers({
   isBoardAdmin() {
   isBoardAdmin() {
     return ReactiveCache.getCurrentUser().isBoardAdmin();
     return ReactiveCache.getCurrentUser().isBoardAdmin();
-  }
+  },
+  numberFieldsSum() {
+    const list = Template.currentData();
+    if (!list) return 0;
+    const boardId = Session.get('currentBoard');
+    const fields = ReactiveCache.getCustomFields({
+      boardIds: { $in: [boardId] },
+      showSumAtTopOfList: true,
+      type: 'number',
+    });
+    if (!fields || !fields.length) return 0;
+    const cards = ReactiveCache.getCards({ listId: list._id, archived: false });
+    let total = 0;
+    if (cards && cards.length) {
+      cards.forEach(card => {
+        const cfs = (card.customFields || []);
+        fields.forEach(field => {
+          const cf = cfs.find(f => f && f._id === field._id);
+          if (!cf || cf.value === null || cf.value === undefined) return;
+          let v = cf.value;
+          if (typeof v === 'string') {
+            const parsed = parseFloat(v.replace(',', '.'));
+            if (isNaN(parsed)) return;
+            v = parsed;
+          }
+          if (typeof v === 'number' && isFinite(v)) {
+            total += v;
+          }
+        });
+      });
+    }
+    return total;
+  },
+  hasNumberFieldsSum() {
+    const boardId = Session.get('currentBoard');
+    const fields = ReactiveCache.getCustomFields({
+      boardIds: { $in: [boardId] },
+      showSumAtTopOfList: true,
+      type: 'number',
+    });
+    return !!(fields && fields.length);
+  },
 });
 });
 
 
 Template.listActionPopup.helpers({
 Template.listActionPopup.helpers({

+ 1 - 1
client/components/lists/minilist.jade

@@ -3,6 +3,6 @@ template(name="minilist")
     class="minicard-{{colorClass}}")
     class="minicard-{{colorClass}}")
     .minicard-title
     .minicard-title
       .handle
       .handle
-        .fa.fa-arrows
+        span.drag-handle(title="{{_ 'dragList'}}") ↕️
       +viewer
       +viewer
         = title
         = title

+ 3 - 3
client/components/main/layouts.css

@@ -527,7 +527,7 @@ a:not(.disabled).is-active i.fa {
   
   
   /* Board canvas */
   /* Board canvas */
   .board-canvas {
   .board-canvas {
-    padding: 8px;
+    padding: 0 8px 8px 0;
     overflow-x: auto;
     overflow-x: auto;
     -webkit-overflow-scrolling: touch;
     -webkit-overflow-scrolling: touch;
   }
   }
@@ -675,7 +675,7 @@ a:not(.disabled).is-active i.fa {
   }
   }
   
   
   .board-canvas {
   .board-canvas {
-    padding: 12px;
+    padding: 0 12px 12px 0;
   }
   }
   
   
   #header {
   #header {
@@ -756,7 +756,7 @@ a:not(.disabled).is-active i.fa {
 .inline-input {
 .inline-input {
   height: 37px;
   height: 37px;
   margin: 8px 10px 0 0;
   margin: 8px 10px 0 0;
-  width: 50px;
+  width: 100px;
 }
 }
 .select-authentication {
 .select-authentication {
   width: 100%;
   width: 100%;

+ 4 - 4
client/components/notifications/notification.css

@@ -20,10 +20,10 @@
   height: 3vw;
   height: 3vw;
 }
 }
 #notifications-drawer .notification .read-status .activity-type {
 #notifications-drawer .notification .read-status .activity-type {
-  margin: 2vh 0 0;
-  width: 2.2vw;
-  height: 2.2vw;
-  font-size: clamp(14px, 2.5vw, 17px);
+  margin: 8px 0 0;
+  width: 1.2em;
+  height: 1.2em;
+  font-size: clamp(14px, 2vw, 17px);
   display: block;
   display: block;
   color: #bbb;
   color: #bbb;
 }
 }

+ 1 - 1
client/components/notifications/notification.jade

@@ -7,4 +7,4 @@ template(name='notification')
       +activity(activity=activityData mode='none')
       +activity(activity=activityData mode='none')
     if read
     if read
       .remove
       .remove
-        a.fa.fa-trash
+        a(title="{{_ 'delete'}}") 🗑️

+ 14 - 14
client/components/notifications/notificationIcon.jade

@@ -1,8 +1,8 @@
 template(name='notificationIcon')
 template(name='notificationIcon')
   if($in activityType 'deleteAttachment' 'addAttachment')
   if($in activityType 'deleteAttachment' 'addAttachment')
-    i.fa.fa-paperclip.activity-type(title="attachment")
+    span.activity-type(title="attachment") 📎
   else if($in activityType 'createBoard' 'importBoard')
   else if($in activityType 'createBoard' 'importBoard')
-    i.fa.fa-chalkboard.activity-type(title="board")
+    span.activity-type(title="board") 🗂️
 
 
   else if($in activityType 'createCard' 'importCard' 'moveCard')
   else if($in activityType 'createCard' 'importCard' 'moveCard')
     +cardNotificationIcon
     +cardNotificationIcon
@@ -19,17 +19,17 @@ template(name='notificationIcon')
     //- DRY and consistant
     //- DRY and consistant
 
 
   else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
   else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
-    i.fa.fa-check-square.activity-type(title="checklist item")
+    span.activity-type(title="checklist item") ☑️
   else if($in activityType 'addComment')
   else if($in activityType 'addComment')
-    i.fa.fa-comment-o.activity-type(title="comment")
+    span.activity-type(title="comment") 💬
   else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
   else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
-    i.fa.fa-code.activity-type(title="custom field")
+    span.activity-type(title="custom field") 🧩
   else if($in activityType 'addedLabel' 'removedLabel')
   else if($in activityType 'addedLabel' 'removedLabel')
-    i.fa.fa-tag.activity-type(title="label")
+    span.activity-type(title="label") 🏷️
   else if($in activityType 'a-startAt' 'a-receivedAt')
   else if($in activityType 'a-startAt' 'a-receivedAt')
-    i.fa.fa-clock-o.activity-type(title="date")
+    span.activity-type(title="date") ⏰
   else if($in activityType 'a-dueAt' 'a-endAt')
   else if($in activityType 'a-dueAt' 'a-endAt')
-    i.fa.fa-clock-o.activity-type(title="date")
+    span.activity-type(title="date") ⏰
 
 
   else if($in activityType 'createList' 'removeList' 'archivedList')
   else if($in activityType 'createList' 'removeList' 'archivedList')
     +listNotificationIcon
     +listNotificationIcon
@@ -41,17 +41,17 @@ template(name='notificationIcon')
     //- elswhere in the app we use fa-trello to indicate lists...
     //- elswhere in the app we use fa-trello to indicate lists...
     //- i personally like fa-columns a bit better
     //- i personally like fa-columns a bit better
   else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember')
   else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember')
-    i.fa.fa-user.activity-type(title="member")
+    span.activity-type(title="member") 👤
   else if($in activityType 'createSwimlane' 'archivedSwimlane')
   else if($in activityType 'createSwimlane' 'archivedSwimlane')
-    i.fa.fa-th-large.activity-type(title="swimlane")
+    span.activity-type(title="swimlane") 🧭
   else
   else
-    i.fa.fa-bug.activity-type(title="can't find icon for #{activityType}")
+    span.activity-type(title="can't find icon for #{activityType}") 🐞
 
 
 template(name='cardNotificationIcon')
 template(name='cardNotificationIcon')
-  i.fa.fa-clone.activity-type(title="card")
+  span.activity-type(title="card") 🗒️
 
 
 template(name='checklistNotificationIcon')
 template(name='checklistNotificationIcon')
-  i.fa.fa-list.activity-type(title="checklist")
+  span.activity-type(title="checklist") 📝
 
 
 template(name='listNotificationIcon')
 template(name='listNotificationIcon')
-  i.fa.fa-columns.activity-type(title="list")
+  span.activity-type(title="list") 📋

+ 0 - 3
client/components/notifications/notificationsDrawer.css

@@ -55,9 +55,6 @@ section#notifications-drawer .remove-read {
 section#notifications-drawer .remove-read:hover {
 section#notifications-drawer .remove-read:hover {
   color: #eb4646 !important;
   color: #eb4646 !important;
 }
 }
-section#notifications-drawer .remove-read:hover i.fa {
-  color: inherit;
-}
 section#notifications-drawer ul.notifications {
 section#notifications-drawer ul.notifications {
   display: block;
   display: block;
   padding: 0px 16px 0px 16px;
   padding: 0px 16px 0px 16px;

+ 2 - 2
client/components/notifications/notificationsDrawer.jade

@@ -8,7 +8,7 @@ template(name='notificationsDrawer')
       h5 {{_ 'notifications'}}
       h5 {{_ 'notifications'}}
         if($gt unreadNotifications 0)
         if($gt unreadNotifications 0)
           |(#{unreadNotifications})
           |(#{unreadNotifications})
-      a.fa.fa-times-thin.close
+      a.close
     ul.notifications
     ul.notifications
       each transformedProfile.notifications
       each transformedProfile.notifications
         +notification(activityData=activityObj index=dbIndex read=read)
         +notification(activityData=activityObj index=dbIndex read=read)
@@ -16,5 +16,5 @@ template(name='notificationsDrawer')
       a.all-read {{_ 'mark-all-as-read'}}
       a.all-read {{_ 'mark-all-as-read'}}
     if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
     if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
       a.remove-read
       a.remove-read
-        i.fa.fa-trash
+        | 🗑️
         | {{_ 'remove-all-read'}}
         | {{_ 'remove-all-read'}}

+ 11 - 11
client/components/sidebar/sidebarFilters.jade

@@ -36,7 +36,7 @@ template(name="filterSidebar")
             else
             else
               span.quiet {{_ "label-default" (_ (concat "color-" color))}}
               span.quiet {{_ "label-default" (_ (concat "color-" color))}}
           if Filter.labelIds.isSelected _id
           if Filter.labelIds.isSelected _id
-            i.fa.fa-check
+            | ✅
   hr
   hr
   h3
   h3
     | 👥
     | 👥
@@ -68,7 +68,7 @@ template(name="filterSidebar")
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-no-assignee'}}
           | {{_ 'filter-no-assignee'}}
         if Filter.assignees.isSelected undefined
         if Filter.assignees.isSelected undefined
-          i.fa.fa-check
+          | ✅
     each currentBoard.activeMembers
     each currentBoard.activeMembers
       with getUser userId
       with getUser userId
         li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
         li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
@@ -90,37 +90,37 @@ template(name="filterSidebar")
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-no-due-date' }}
           | {{_ 'filter-no-due-date' }}
         if Filter.dueAt.isSelected 'noDate'
         if Filter.dueAt.isSelected 'noDate'
-          i.fa.fa-check
+          | ✅
     li(class="{{#if Filter.dueAt.isSelected 'past'}}active{{/if}}")
     li(class="{{#if Filter.dueAt.isSelected 'past'}}active{{/if}}")
       a.name.js-toggle-overdue-filter
       a.name.js-toggle-overdue-filter
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-overdue' }}
           | {{_ 'filter-overdue' }}
         if Filter.dueAt.isSelected 'past'
         if Filter.dueAt.isSelected 'past'
-          i.fa.fa-check
+          | ✅
     li(class="{{#if Filter.dueAt.isSelected 'today'}}active{{/if}}")
     li(class="{{#if Filter.dueAt.isSelected 'today'}}active{{/if}}")
       a.name.js-toggle-due-today-filter
       a.name.js-toggle-due-today-filter
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-due-today' }}
           | {{_ 'filter-due-today' }}
         if Filter.dueAt.isSelected 'today'
         if Filter.dueAt.isSelected 'today'
-          i.fa.fa-check
+          | ✅
     li(class="{{#if Filter.dueAt.isSelected 'tomorrow'}}active{{/if}}")
     li(class="{{#if Filter.dueAt.isSelected 'tomorrow'}}active{{/if}}")
       a.name.js-toggle-due-tomorrow-filter
       a.name.js-toggle-due-tomorrow-filter
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-due-tomorrow' }}
           | {{_ 'filter-due-tomorrow' }}
         if Filter.dueAt.isSelected 'tomorrow'
         if Filter.dueAt.isSelected 'tomorrow'
-          i.fa.fa-check
+          | ✅
     li(class="{{#if Filter.dueAt.isSelected 'thisweek'}}active{{/if}}")
     li(class="{{#if Filter.dueAt.isSelected 'thisweek'}}active{{/if}}")
       a.name.js-toggle-due-this-week-filter
       a.name.js-toggle-due-this-week-filter
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-due-this-week' }}
           | {{_ 'filter-due-this-week' }}
         if Filter.dueAt.isSelected 'thisweek'
         if Filter.dueAt.isSelected 'thisweek'
-          i.fa.fa-check
+          | ✅
     li(class="{{#if Filter.dueAt.isSelected 'nextweek'}}active{{/if}}")
     li(class="{{#if Filter.dueAt.isSelected 'nextweek'}}active{{/if}}")
       a.name.js-toggle-due-next-week-filter
       a.name.js-toggle-due-next-week-filter
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-due-next-week' }}
           | {{_ 'filter-due-next-week' }}
         if Filter.dueAt.isSelected 'nextweek'
         if Filter.dueAt.isSelected 'nextweek'
-          i.fa.fa-check
+          | ✅
   hr
   hr
   h3
   h3
     | 📋
     | 📋
@@ -138,7 +138,7 @@ template(name="filterSidebar")
           span.sidebar-list-item-description
           span.sidebar-list-item-description
             | {{ name }}
             | {{ name }}
           if Filter.customFields.isSelected _id
           if Filter.customFields.isSelected _id
-            i.fa.fa-check
+            | ✅
   hr
   hr
   h3
   h3
     | {{_ 'other-filters-label'}}
     | {{_ 'other-filters-label'}}
@@ -148,14 +148,14 @@ template(name="filterSidebar")
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-show-archive'}}
           | {{_ 'filter-show-archive'}}
         if Filter.archive.isSelected _id
         if Filter.archive.isSelected _id
-          i.fa.fa-check
+          | ✅
   ul.sidebar-list
   ul.sidebar-list
     li(class="{{#if Filter.hideEmpty.isSelected _id}}active{{/if}}")
     li(class="{{#if Filter.hideEmpty.isSelected _id}}active{{/if}}")
       a.name.js-toggle-hideEmpty-filter
       a.name.js-toggle-hideEmpty-filter
         span.sidebar-list-item-description
         span.sidebar-list-item-description
           | {{_ 'filter-hide-empty'}}
           | {{_ 'filter-hide-empty'}}
         if Filter.hideEmpty.isSelected _id
         if Filter.hideEmpty.isSelected _id
-          i.fa.fa-check
+          | ✅
   hr
   hr
   h3 {{_ 'advanced-filter-label'}}
   h3 {{_ 'advanced-filter-label'}}
   input.js-field-advanced-filter(type="text")
   input.js-field-advanced-filter(type="text")

+ 2 - 11
client/components/swimlanes/swimlaneHeader.jade

@@ -28,23 +28,14 @@ template(name="swimlaneFixedHeader")
         unless currentUser.isWorker
         unless currentUser.isWorker
           a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
           a.js-open-add-swimlane-menu.swimlane-header-plus-icon(title="{{_ 'add-swimlane'}}")
             | ➕
             | ➕
-          a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
-            | ☰
-          //// TODO: Collapse Swimlane: make button working, etc.
-          //unless collapsed
-          //  a.js-collapse-swimlane(title="{{_ 'collapse'}}")
-          //    i.fa.fa-arrow-down.swimlane-header-collapse-down
-          //    ⬆️.swimlane-header-collapse-up
-          //if collapsed
-          //  a.js-collapse-swimlane(title="{{_ 'uncollapse'}}")
-          //    ⬆️.swimlane-header-collapse-up
-          //    i.fa.fa-arrow-down.swimlane-header-collapse-down
           unless isTouchScreen
           unless isTouchScreen
             a.swimlane-header-handle.handle.js-swimlane-header-handle
             a.swimlane-header-handle.handle.js-swimlane-header-handle
               | ↕️
               | ↕️
           if isTouchScreen
           if isTouchScreen
             a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
             a.swimlane-header-miniscreen-handle.handle.js-swimlane-header-handle
               | ↕️
               | ↕️
+          a.js-open-swimlane-menu(title="{{_ 'swimlaneActionPopup-title'}}")
+            | ☰
 
 
 template(name="editSwimlaneTitleForm")
 template(name="editSwimlaneTitleForm")
   .list-composer
   .list-composer

+ 24 - 19
client/components/swimlanes/swimlanes.css

@@ -54,12 +54,14 @@
   width: 100%;
   width: 100%;
   min-width: 100%;
   min-width: 100%;
   position: relative;
   position: relative;
-  overflow: hidden;
+  overflow: visible;
   min-height: 33px;
   min-height: 33px;
+  padding: 0;
+  margin: 0;
 }
 }
 .swimlane .swimlane-header-wrap .swimlane-header {
 .swimlane .swimlane-header-wrap .swimlane-header {
   font-size: 14px;
   font-size: 14px;
-  padding: 5px 5px;
+  padding: 0;
   font-weight: bold;
   font-weight: bold;
   min-height: 33px;
   min-height: 33px;
   width: 100%;
   width: 100%;
@@ -74,30 +76,39 @@
 }
 }
 .swimlane .swimlane-header-wrap .swimlane-header-menu {
 .swimlane .swimlane-header-wrap .swimlane-header-menu {
   position: absolute;
   position: absolute;
-  padding: 5px 5px;
+  top: 0;
+  left: 0;
+  padding: 0;
+  margin: 0;
   font-size: 22px;
   font-size: 22px;
+  line-height: 1;
   z-index: 20;
   z-index: 20;
   pointer-events: auto;
   pointer-events: auto;
 }
 }
+.swimlane .swimlane-header-wrap .swimlane-header-menu .js-open-swimlane-menu {
+  top: calc(50% + 6px);
+  padding: 5px;
+  display: inline-block;
+  margin-left: 74px;
+}
 @media print {
 @media print {
   .swimlane .swimlane-header-wrap .swimlane-header-menu {
   .swimlane .swimlane-header-wrap .swimlane-header-menu {
     display: none;
     display: none;
   }
   }
 }
 }
 .swimlane .swimlane-header-wrap .swimlane-header-plus-icon {
 .swimlane .swimlane-header-wrap .swimlane-header-plus-icon {
-  margin-left: 5px;
-  padding-right: 20px;
+  top: calc(50% + 6px);
+  padding: 5px;
   font-size: 22px;
   font-size: 22px;
 }
 }
 .swimlane .swimlane-header-wrap .swimlane-header-menu-icon {
 .swimlane .swimlane-header-wrap .swimlane-header-menu-icon {
-  padding-right: 20px;
+  top: calc(50% + 6px);
+  padding: 5px;
   font-size: 22px;
   font-size: 22px;
 }
 }
 .swimlane .swimlane-header-wrap .swimlane-header-handle {
 .swimlane .swimlane-header-wrap .swimlane-header-handle {
-  position: absolute;
-  padding: 7px;
-  top: 50%;
-  left: 230px;
+  top: calc(50% + 2px);
+  padding: 2px;
   font-size: clamp(16px, 3vw, 20px);
   font-size: clamp(16px, 3vw, 20px);
   transform: translateY(-50%);
   transform: translateY(-50%);
   display: flex;
   display: flex;
@@ -109,22 +120,16 @@
 }
 }
 .swimlane .swimlane-header-wrap .swimlane-header-miniscreen-handle {
 .swimlane .swimlane-header-wrap .swimlane-header-miniscreen-handle {
   position: absolute;
   position: absolute;
-  padding: 7px;
-  top: 50%;
+  padding: 2px;
+  top: calc(50% + 2px);
   transform: translateY(-50%);
   transform: translateY(-50%);
-  right: 10px;
+  right: 60px;
   font-size: 24px;
   font-size: 24px;
   cursor: move;
   cursor: move;
   z-index: 15;
   z-index: 15;
   pointer-events: auto;
   pointer-events: auto;
 }
 }
 
 
-/* Safety: ensure wrapper is interactive and above list content */
-.swimlane .swimlane-header-wrap {
-  position: relative;
-  z-index: 9;
-  pointer-events: auto;
-}
 #js-swimlane-height-edit .swimlane-height-error {
 #js-swimlane-height-edit .swimlane-height-error {
   display: none;
   display: none;
 }
 }

+ 4 - 4
client/components/users/userAvatar.css

@@ -3,8 +3,8 @@
   display: block;
   display: block;
   position: relative;
   position: relative;
   float: left;
   float: left;
-  height: 30px;
-  width: 30px;
+  height: clamp(24px, 3.5vw, 36px);
+  width: clamp(24px, 3.5vw, 36px);
   margin: .3vh;
   margin: .3vh;
   cursor: pointer;
   cursor: pointer;
   user-select: none;
   user-select: none;
@@ -111,7 +111,7 @@
   padding-top: 0;
   padding-top: 0;
 }
 }
 .mini-profile-info .member {
 .mini-profile-info .member {
-  width: 50px;
-  height: 50px;
+  width: clamp(40px, 5vw, 60px);
+  height: clamp(40px, 5vw, 60px);
   margin-right: 10px;
   margin-right: 10px;
 }
 }