mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Let Board Settings reorder the card detail view's Description, Custom
Fields, Labels, Dates and Members sections. The opened card always rendered its sections in one fixed order, with no way to move Description earlier or have Custom Fields follow it. Boards now store a cardFieldOrder array; models/lib/cardFieldOrder.js resolves it against the historical default order, dropping unknown keys and filling in any missing section so a stale or partial value can never hide or duplicate a section. cardDetails.jade renders these five sections from that resolved order instead of a hardcoded sequence, each now its own template (cardFieldSectionLabels, ...Dates, ...Members, ...CustomFields, ...Description) included from an each loop; Dependencies +Sort stay a fixed appendage right after Members and Vote+Poker stay one right after Custom Fields, so a board that never touches the setting renders exactly what it always did. Checklists, Attachments and Activity are left out of this pass - they sit in their own flex/right-column layout further down the template, and moving them was not needed for the issue's ask (move Description earlier, custom fields after it) and would add layout risk this environment cannot verify visually. Board Settings / Card Settings gets a new "Card field order" list with up/down buttons per row. Up/down rather than a drag library: this codebase's existing jQuery-ui-sortable usages (list/swimlane/board reordering) are heavier drag-and-drop over board layout, not a simple settings list, so buttons are the simpler, safer mechanism here. Thanks to mimZD and xet7 ! Closes #4448,
This commit is contained in:
1 parent
59f7d61df7
commit
131514d614
7 files changed
+848
-397
No files matched your search
@@ -136,353 +136,447 @@ template(name="cardDetails")
|
||||
.card-details-left
|
||||
|
||||
.card-details-items
|
||||
.card-details-group.card-details-group-labels
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsLabels
|
||||
.card-details-item.card-details-item-labels
|
||||
+cardSectionHeader(section="labels" icon="fa-tags" label="labels" noRule=true)
|
||||
if isSectionOpen "labels"
|
||||
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
|
||||
each labels
|
||||
span.card-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
//- #4448: render the reorderable card-detail sections (Labels, Dates,
|
||||
Members, Custom Fields, Description) in the board's stored order,
|
||||
falling back to the historical fixed order. Dependencies+Sort stay a
|
||||
fixed appendage right after Members, and Vote+Poker stay a fixed
|
||||
appendage right after Custom Fields, so a board that never touches
|
||||
the setting renders exactly what it always has.
|
||||
models/lib/cardFieldOrder.js, client/components/sidebar/sidebar.jade
|
||||
each orderedCardFieldSections
|
||||
if $eq this "labels"
|
||||
+cardFieldSectionLabels
|
||||
if $eq this "dates"
|
||||
+cardFieldSectionDates
|
||||
if $eq this "members"
|
||||
+cardFieldSectionMembers
|
||||
+cardFieldSectionDependenciesAndSort
|
||||
if $eq this "customFields"
|
||||
+cardFieldSectionCustomFields
|
||||
+cardFieldSectionVoteAndPoker
|
||||
if $eq this "description"
|
||||
+cardFieldSectionDescription
|
||||
|
||||
.card-checklist-attachmentGalleries
|
||||
.card-checklist-attachmentGallery.card-checklists
|
||||
if currentBoard.allowsChecklists
|
||||
+cardSectionHeader(section="checklists" icon="fa-check" label="checklists")
|
||||
if isSectionOpen "checklists"
|
||||
//- Total checklist item count (finished/total) shown above all
|
||||
checklists. The board flag is a cheap boolean, so when it is off
|
||||
the checklist counting below is never evaluated.
|
||||
if currentBoard.allowsChecklistCountBadgeOnCard
|
||||
if checklists.length
|
||||
h4.card-details-item-subtitle
|
||||
| {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||
+checklists(cardId = _id card = this)
|
||||
if currentBoard.allowsSubtasks
|
||||
+cardSectionHeader(section="subtasks" icon="fa-sitemap" label="subtasks"
|
||||
menuClass="js-open-subtasks-settings" menuTitle="subtask-settings")
|
||||
if isSectionOpen "subtasks"
|
||||
+subtasks(cardId = _id)
|
||||
if currentBoard.allowsAttachments
|
||||
+cardSectionHeader(section="attachments" icon="fa-paperclip" label="attachments"
|
||||
count=attachmentCount)
|
||||
if isSectionOpen "attachments"
|
||||
if Meteor.settings.public.attachmentsUploadMaxSize
|
||||
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
|
||||
br
|
||||
if Meteor.settings.public.attachmentsUploadMimeTypes
|
||||
| {{_ 'allowed-upload-filetypes'}} {{Meteor.settings.public.attachmentsUploadMimeTypes}}
|
||||
br
|
||||
| {{_ 'invalid-file'}}
|
||||
.card-checklist-attachmentGallery.card-attachmentGallery
|
||||
+attachmentGallery
|
||||
|
||||
if currentBoard.allowsComments
|
||||
.comment-title
|
||||
+cardSectionHeader(section="comments" icon="fa-comment-o" label="comments")
|
||||
if isSectionOpen "comments"
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isNoComments
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
+commentForm
|
||||
+comments
|
||||
|
||||
.card-details-right
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
//- OUTSIDE .activity-title, which is `display: flex`: a rule inside it
|
||||
becomes a flex item and renders as a short line beside the heading
|
||||
instead of a separator above it.
|
||||
The eye that used to sit here showed and hid exactly what the caret
|
||||
shows and hides, so there were two controls for one thing and they
|
||||
could disagree. The caret is the one that stayed, and this section is
|
||||
the one that starts CLOSED - a card is opened to read the card, not
|
||||
its history.
|
||||
+cardSectionHeader(section="activities" icon="fa-history" label="activities")
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
if isSectionOpen "activities"
|
||||
if isLoaded.get
|
||||
if isLinkedCard
|
||||
+activities(card=this mode="linkedcard")
|
||||
else if isLinkedBoard
|
||||
+activities(card=this mode="linkedboard")
|
||||
else
|
||||
+activities(card=this mode="card")
|
||||
|
||||
template(name="cardFieldSectionLabels")
|
||||
.card-details-group.card-details-group-labels
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsLabels
|
||||
.card-details-item.card-details-item-labels
|
||||
+cardSectionHeader(section="labels" icon="fa-tags" label="labels" noRule=true)
|
||||
if isSectionOpen "labels"
|
||||
.card-details-item.card-details-item-stickers
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sticky-note-o
|
||||
| {{_ 'stickers'}}
|
||||
.card-details-item-content.card-stickers
|
||||
each stickers
|
||||
span.card-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if canModifyCard
|
||||
a.card-sticker-remove.js-remove-sticker(data-index="{{@index}}" title="{{_ 'remove-sticker'}}")
|
||||
i.fa.fa-times
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-stickers(title="{{_ 'add-sticker'}}")
|
||||
i.fa.fa-plus
|
||||
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
|
||||
each labels
|
||||
span.card-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title=name)
|
||||
+viewer
|
||||
= name
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-item.card-details-item-location
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-map-marker
|
||||
| {{_ 'location'}}
|
||||
.card-details-item-content.card-locations
|
||||
each getLocations
|
||||
.card-location
|
||||
.card-location-info(class="{{#if canModifyCard}}js-edit-location{{/if}}" data-location-id="{{_id}}")
|
||||
if name
|
||||
.card-location-name= name
|
||||
if address
|
||||
.card-location-address= address
|
||||
if hasCoordinates
|
||||
a.card-location-map(href=mapUrl target="_blank" rel="noopener noreferrer")
|
||||
i.fa.fa-map-o
|
||||
= ' '
|
||||
| {{_ 'location-open-map'}}
|
||||
if canModifyCard
|
||||
a.card-location-remove.js-remove-location(data-location-id="{{_id}}" title="{{_ 'delete'}}")
|
||||
i.fa.fa-times
|
||||
if isSectionOpen "labels"
|
||||
.card-details-item.card-details-item-stickers
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sticky-note-o
|
||||
| {{_ 'stickers'}}
|
||||
.card-details-item-content.card-stickers
|
||||
each stickers
|
||||
span.card-sticker(title="{{#if name}}{{name}}{{else}}{{icon}}{{/if}}")
|
||||
i.fa(class="fa-{{icon}} {{#if highlight}}sticker-{{highlight}}{{/if}}")
|
||||
if canModifyCard
|
||||
a.card-sticker-remove.js-remove-sticker(data-index="{{@index}}" title="{{_ 'remove-sticker'}}")
|
||||
i.fa.fa-times
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-stickers(title="{{_ 'add-sticker'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-item.card-details-item-location
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-map-marker
|
||||
| {{_ 'location'}}
|
||||
.card-details-item-content.card-locations
|
||||
each getLocations
|
||||
.card-location
|
||||
.card-location-info(class="{{#if canModifyCard}}js-edit-location{{/if}}" data-location-id="{{_id}}")
|
||||
if name
|
||||
.card-location-name= name
|
||||
if address
|
||||
.card-location-address= address
|
||||
if hasCoordinates
|
||||
a.card-location-map(href=mapUrl target="_blank" rel="noopener noreferrer")
|
||||
i.fa.fa-map-o
|
||||
= ' '
|
||||
| {{_ 'location-open-map'}}
|
||||
if canModifyCard
|
||||
a.card-location-remove.js-remove-location(data-location-id="{{_id}}" title="{{_ 'delete'}}")
|
||||
i.fa.fa-times
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-location(title="{{_ 'add-location'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
//- The card's button row - it sat here before the fields were grouped,
|
||||
between the Labels group and the dates, and belongs to the card rather
|
||||
than to any group.
|
||||
+cardButtons(_id=_id boardId=boardId)
|
||||
|
||||
template(name="cardFieldSectionDates")
|
||||
.card-details-group.card-details-group-date-format
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
.card-details-item.card-details-item-date-format
|
||||
+cardSectionHeader(section="date-format" icon="fa-calendar" label="date-format" noRule=true)
|
||||
if isSectionOpen "date-format"
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
|
||||
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
|
||||
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
|
||||
|
||||
if isSectionOpen "date-format"
|
||||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-out
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-add-location(title="{{_ 'add-location'}}")
|
||||
a.card-label.add-label.js-received-date
|
||||
i.fa.fa-plus
|
||||
|
||||
//- The card's button row - it sat here before the fields were grouped,
|
||||
between the Labels group and the dates, and belongs to the card rather
|
||||
than to any group.
|
||||
+cardButtons(_id=_id boardId=boardId)
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-start
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-start-date
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-group.card-details-group-date-format
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.hasAnyAllowsDate
|
||||
.card-details-item.card-details-item-date-format
|
||||
+cardSectionHeader(section="date-format" icon="fa-calendar" label="date-format" noRule=true)
|
||||
if isSectionOpen "date-format"
|
||||
.card-details-item-content
|
||||
select.js-date-format-selector
|
||||
option(value="YYYY-MM-DD" selected="{{#if isDateFormat 'YYYY-MM-DD'}}selected{{/if}}") {{_ 'date-format-yyyy-mm-dd'}}
|
||||
option(value="DD-MM-YYYY" selected="{{#if isDateFormat 'DD-MM-YYYY'}}selected{{/if}}") {{_ 'date-format-dd-mm-yyyy'}}
|
||||
option(value="MM-DD-YYYY" selected="{{#if isDateFormat 'MM-DD-YYYY'}}selected{{/if}}") {{_ 'date-format-mm-dd-yyyy'}}
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-due-date
|
||||
i.fa.fa-plus
|
||||
if dueDateChangeCount
|
||||
.card-details-due-date-changes(title="{{_ 'due-date-changes'}}")
|
||||
| {{_ 'due-date-changed-times' dueDateChangeCount}}
|
||||
|
||||
if isSectionOpen "date-format"
|
||||
if currentBoard.allowsReceivedDate
|
||||
.card-details-item.card-details-item-received
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-sign-out
|
||||
| {{_ 'card-received'}}
|
||||
if getReceived
|
||||
+cardReceivedDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-received-date
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsStartDate
|
||||
.card-details-item.card-details-item-start
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-start
|
||||
| {{_ 'card-start'}}
|
||||
if getStart
|
||||
+cardStartDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-start-date
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsDueDate
|
||||
.card-details-item.card-details-item-due
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'card-due'}}
|
||||
if getDue
|
||||
+cardDueDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-due-date
|
||||
i.fa.fa-plus
|
||||
if dueDateChangeCount
|
||||
.card-details-due-date-changes(title="{{_ 'due-date-changes'}}")
|
||||
| {{_ 'due-date-changed-times' dueDateChangeCount}}
|
||||
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-end
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-end-date
|
||||
i.fa.fa-plus
|
||||
|
||||
.card-details-group.card-details-group-members
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsMembers
|
||||
.card-details-item.card-details-item-members
|
||||
+cardSectionHeader(section="members" icon="fa-users" label="members" noRule=true)
|
||||
if isSectionOpen "members"
|
||||
each userId in getMembers
|
||||
+userAvatar(userId=userId cardId=_id)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
|
||||
i.fa.fa-plus
|
||||
if currentBoard.allowsEndDate
|
||||
.card-details-item.card-details-item-end
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-hourglass-end
|
||||
| {{_ 'card-end'}}
|
||||
if getEnd
|
||||
+cardEndDate(card=this canModifyCard=canModifyCard)
|
||||
else
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.card-label.add-label.js-end-date
|
||||
i.fa.fa-plus
|
||||
|
||||
template(name="cardFieldSectionMembers")
|
||||
.card-details-group.card-details-group-members
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsMembers
|
||||
.card-details-item.card-details-item-members
|
||||
+cardSectionHeader(section="members" icon="fa-users" label="members" noRule=true)
|
||||
if isSectionOpen "members"
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'assignee'}}
|
||||
each userId in getAssignees
|
||||
+userAvatar(userId=userId cardId=_id assignee=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
if currentUser.isWorker
|
||||
unless assigneeSelected
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
each userId in getMembers
|
||||
+userAvatar(userId=userId cardId=_id)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
if currentBoard.allowsCreator
|
||||
.card-details-item.card-details-item-creator
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'creator'}}
|
||||
if isSectionOpen "members"
|
||||
if currentBoard.allowsAssignee
|
||||
.card-details-item.card-details-item-assignees
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'assignee'}}
|
||||
each userId in getAssignees
|
||||
+userAvatar(userId=userId cardId=_id assignee=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if canModifyCard
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
if currentUser.isWorker
|
||||
unless assigneeSelected
|
||||
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
|
||||
i.fa.fa-plus
|
||||
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
if currentBoard.allowsCreator
|
||||
.card-details-item.card-details-item-creator
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user
|
||||
| {{_ 'creator'}}
|
||||
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
| {{! XXX Hack to hide syntaxic coloration /// }}
|
||||
|
||||
|
||||
//.card-details-items
|
||||
//if assigneeSelected
|
||||
//.card-details-items
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-shopping-cart
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-requester")
|
||||
+editCardRequesterForm
|
||||
else
|
||||
each userId in getRequesters
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getRequestedBy
|
||||
+viewer
|
||||
= getRequestedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-requester(title="{{_ 'requested-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getRequestedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
//.card-details-items
|
||||
//if assigneeSelected
|
||||
//.card-details-items
|
||||
if currentBoard.allowsRequestedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-shopping-cart
|
||||
| {{_ 'requested-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-requester")
|
||||
+editCardRequesterForm
|
||||
else
|
||||
each userId in getRequesters
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getRequestedBy
|
||||
+viewer
|
||||
= getRequestedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-requester(title="{{_ 'requested-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getRequestedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getRequestedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
else
|
||||
each userId in getRequesters
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
if getRequestedBy
|
||||
+viewer
|
||||
= getRequestedBy
|
||||
|
||||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user-plus
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-assigner")
|
||||
+editCardAssignerForm
|
||||
else
|
||||
each userId in getAssigners
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getAssignedBy
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-assigner(title="{{_ 'assigned-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getAssignedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
if currentBoard.allowsAssignedBy
|
||||
.card-details-item.card-details-item-name
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-user-plus
|
||||
| {{_ 'assigned-by'}}
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
+inlinedForm(classNames="js-card-details-assigner")
|
||||
+editCardAssignerForm
|
||||
else
|
||||
each userId in getAssigners
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
+userAvatar(userId=userId cardId=_id noRemove=true)
|
||||
if getAssignedBy
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
.card-details-person-controls
|
||||
a.member.add-member.card-details-item-add-button.js-select-assigner(title="{{_ 'assigned-by'}}")
|
||||
i.fa.fa-plus
|
||||
a.js-open-inlined-form(
|
||||
title="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}"
|
||||
aria-label="{{#if getAssignedBy}}{{_ 'edit'}}{{else}}{{_ 'add'}}{{/if}}")
|
||||
if getAssignedBy
|
||||
i.fa.fa-pencil-square-o(aria-hidden="true")
|
||||
else
|
||||
i.fa.fa-plus(aria-hidden="true")
|
||||
else
|
||||
each userId in getAssigners
|
||||
+userAvatar(userId=userId noRemove=true)
|
||||
if getAssignedBy
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
|
||||
.card-details-group.card-details-group-dependencies
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
.card-details-item.card-details-item-dependencies
|
||||
+cardSectionHeader(section="dependencies" icon="fa-link" label="card-dependencies" noRule=true)
|
||||
if isSectionOpen "dependencies"
|
||||
.card-details-dependencies-list
|
||||
each getDependencyCards
|
||||
.card-dependency(data-target-id="{{card._id}}")
|
||||
i.fa(class="fa-{{icon}}" style="color:{{color}}")
|
||||
a.card-dependency-link(href="{{linkUrl}}")
|
||||
+viewer
|
||||
= card.title
|
||||
if canModifyCard
|
||||
select.js-dependency-type(data-target-id="{{card._id}}")
|
||||
each typeOption
|
||||
if selected
|
||||
option(value="{{id}}" selected="selected") {{_ label}}
|
||||
else
|
||||
option(value="{{id}}") {{_ label}}
|
||||
input.js-dependency-color(type="color" value="{{color}}" data-target-id="{{card._id}}" title="{{_ 'dependency-color'}}")
|
||||
a.js-dependency-icon(data-target-id="{{card._id}}" title="{{_ 'dependency-icon'}}")
|
||||
i.fa.fa-pencil
|
||||
a.card-dependency-remove.js-remove-dependency(data-target-id="{{card._id}}" title="{{_ 'remove-dependency'}}")
|
||||
i.fa.fa-times
|
||||
else
|
||||
span.card-dependency-type {{_ typeLabel}}
|
||||
if canModifyCard
|
||||
a.js-add-dependency(title="{{_ 'add-dependency'}}")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-dependency'}}
|
||||
|
||||
.card-details-group.card-details-group-sort
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
+cardSectionHeader(section="sort" icon="fa-sort-numeric-asc" label="sort" noRule=true)
|
||||
if isSectionOpen "sort"
|
||||
template(name="cardFieldSectionDependenciesAndSort")
|
||||
.card-details-group.card-details-group-dependencies
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
.card-details-item.card-details-item-dependencies
|
||||
+cardSectionHeader(section="dependencies" icon="fa-link" label="card-dependencies" noRule=true)
|
||||
if isSectionOpen "dependencies"
|
||||
.card-details-dependencies-list
|
||||
each getDependencyCards
|
||||
.card-dependency(data-target-id="{{card._id}}")
|
||||
i.fa(class="fa-{{icon}}" style="color:{{color}}")
|
||||
a.card-dependency-link(href="{{linkUrl}}")
|
||||
+viewer
|
||||
= card.title
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
+editCardSortOrderForm
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
+viewer
|
||||
= sort
|
||||
|
||||
if isSectionOpen "sort"
|
||||
if currentBoard.allowsShowLists
|
||||
.card-details-item.card-details-show-lists
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list
|
||||
| {{_ 'list'}}
|
||||
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
|
||||
each currentSwimlaneListsSorted
|
||||
option(value="{{_id}}" selected="{{#if isCurrentListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
//.card-details-items
|
||||
if getSpentTime
|
||||
.card-details-item.card-details-item-spent
|
||||
if getIsOvertime
|
||||
h3.card-details-item-title
|
||||
| {{_ 'overtime-hours'}}
|
||||
select.js-dependency-type(data-target-id="{{card._id}}")
|
||||
each typeOption
|
||||
if selected
|
||||
option(value="{{id}}" selected="selected") {{_ label}}
|
||||
else
|
||||
option(value="{{id}}") {{_ label}}
|
||||
input.js-dependency-color(type="color" value="{{color}}" data-target-id="{{card._id}}" title="{{_ 'dependency-color'}}")
|
||||
a.js-dependency-icon(data-target-id="{{card._id}}" title="{{_ 'dependency-icon'}}")
|
||||
i.fa.fa-pencil
|
||||
a.card-dependency-remove.js-remove-dependency(data-target-id="{{card._id}}" title="{{_ 'remove-dependency'}}")
|
||||
i.fa.fa-times
|
||||
else
|
||||
h3.card-details-item-title
|
||||
| {{_ 'spent-time-hours'}}
|
||||
+cardSpentTime
|
||||
span.card-dependency-type {{_ typeLabel}}
|
||||
if canModifyCard
|
||||
a.js-add-dependency(title="{{_ 'add-dependency'}}")
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-dependency'}}
|
||||
.card-details-group.card-details-group-sort
|
||||
hr.card-details-section-rule
|
||||
.card-details-group-body
|
||||
if currentBoard.allowsCardSortingByNumber
|
||||
.card-details-item.card-details-sort-order
|
||||
+cardSectionHeader(section="sort" icon="fa-sort-numeric-asc" label="sort" noRule=true)
|
||||
if isSectionOpen "sort"
|
||||
if canModifyCard
|
||||
+inlinedForm(classNames="js-card-details-sort")
|
||||
+editCardSortOrderForm
|
||||
else
|
||||
a.js-open-inlined-form
|
||||
+viewer
|
||||
= sort
|
||||
|
||||
.card-details-item.card-details-item-flow
|
||||
if isSectionOpen "sort"
|
||||
if currentBoard.allowsShowLists
|
||||
.card-details-item.card-details-show-lists
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-list
|
||||
| {{_ 'list'}}
|
||||
select.js-select-card-details-lists(disabled="{{#unless canModifyCard}}disabled{{/unless}}")
|
||||
each currentSwimlaneListsSorted
|
||||
option(value="{{_id}}" selected="{{#if isCurrentListId _id}}selected{{/if}}") {{title}}
|
||||
|
||||
//.card-details-items
|
||||
if getSpentTime
|
||||
.card-details-item.card-details-item-spent
|
||||
if getIsOvertime
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-bolt
|
||||
| {{_ 'flowtime'}}
|
||||
+cardFlowtime
|
||||
|
||||
.card-details-item.card-details-item-pomodoro
|
||||
| {{_ 'overtime-hours'}}
|
||||
else
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'pomodoro'}}
|
||||
+cardPomodoro
|
||||
| {{_ 'spent-time-hours'}}
|
||||
+cardSpentTime
|
||||
|
||||
//.card-details-items
|
||||
//- The heading is here for EVERY card the reader may write to, not only
|
||||
for one that already has a custom field on it. The hamburger at the end
|
||||
of it is the single place custom fields live - the picker of which
|
||||
fields are on this card, a pencil each, and "Add custom field" - so
|
||||
gating the heading on `customFieldsWD`, the card's VALUES, meant a card
|
||||
with none had no way to get one, and a board that had never used them
|
||||
had no way anywhere: the card menu's entry and the Board Settings row
|
||||
were removed with the move, and the sidebar view is opened by nothing.
|
||||
The FIELDS below still come from `customFieldsWD`, so an empty section
|
||||
is an empty section, not a phantom row.
|
||||
if canShowCustomFieldsOnCard
|
||||
//- ONE header for the whole group, outside the loop: a card with six
|
||||
custom fields is one Custom Fields section, not six sections that
|
||||
happen to share a caret and draw six rules. A group like the others,
|
||||
so its rule spans the card and its fields share rows inside it.
|
||||
.card-details-group.card-details-group-custom-fields
|
||||
+cardSectionHeader(section="custom-fields" icon="fa-list-alt" label="custom-fields"
|
||||
menuClass="js-open-custom-fields-settings" menuTitle="custom-fields"
|
||||
layoutToggle=true layoutOnePerRow=customFieldsGrid)
|
||||
if isSectionOpen "custom-fields"
|
||||
.card-details-group-body(class="{{#if customFieldsGrid}}custom-fields-one-per-row{{else}}custom-fields-grid{{/if}}")
|
||||
each customFieldsWD
|
||||
.card-details-item.card-details-item-customfield
|
||||
+cardCustomField
|
||||
.card-details-item.card-details-item-flow
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-bolt
|
||||
| {{_ 'flowtime'}}
|
||||
+cardFlowtime
|
||||
|
||||
.card-details-item.card-details-item-pomodoro
|
||||
h3.card-details-item-title
|
||||
i.fa.fa-clock-o
|
||||
| {{_ 'pomodoro'}}
|
||||
+cardPomodoro
|
||||
|
||||
//.card-details-items
|
||||
|
||||
template(name="cardFieldSectionCustomFields")
|
||||
//- The heading is here for EVERY card the reader may write to, not only
|
||||
for one that already has a custom field on it. The hamburger at the end
|
||||
of it is the single place custom fields live - the picker of which
|
||||
fields are on this card, a pencil each, and "Add custom field" - so
|
||||
gating the heading on `customFieldsWD`, the card's VALUES, meant a card
|
||||
with none had no way to get one, and a board that had never used them
|
||||
had no way anywhere: the card menu's entry and the Board Settings row
|
||||
were removed with the move, and the sidebar view is opened by nothing.
|
||||
The FIELDS below still come from `customFieldsWD`, so an empty section
|
||||
is an empty section, not a phantom row.
|
||||
if canShowCustomFieldsOnCard
|
||||
//- ONE header for the whole group, outside the loop: a card with six
|
||||
custom fields is one Custom Fields section, not six sections that
|
||||
happen to share a caret and draw six rules. A group like the others,
|
||||
so its rule spans the card and its fields share rows inside it.
|
||||
.card-details-group.card-details-group-custom-fields
|
||||
+cardSectionHeader(section="custom-fields" icon="fa-list-alt" label="custom-fields"
|
||||
menuClass="js-open-custom-fields-settings" menuTitle="custom-fields"
|
||||
layoutToggle=true layoutOnePerRow=customFieldsGrid)
|
||||
if isSectionOpen "custom-fields"
|
||||
.card-details-group-body(class="{{#if customFieldsGrid}}custom-fields-one-per-row{{else}}custom-fields-grid{{/if}}")
|
||||
each customFieldsWD
|
||||
.card-details-item.card-details-item-customfield
|
||||
+cardCustomField
|
||||
|
||||
template(name="cardFieldSectionVoteAndPoker")
|
||||
.card-details-group.card-details-group-vote-poker
|
||||
if getVoteQuestion
|
||||
hr
|
||||
.vote-title
|
||||
@@ -513,7 +607,6 @@ template(name="cardDetails")
|
||||
if $eq voteState false
|
||||
i.fa.fa-thumbs-down
|
||||
| {{_ 'vote-against'}}
|
||||
|
||||
if getPokerQuestion
|
||||
hr
|
||||
.poker-title
|
||||
@@ -714,7 +807,8 @@ template(name="cardDetails")
|
||||
| {{_ 'set-estimation'}}
|
||||
input(type=text,autofocus value=getPokerEstimation,id="pokerEstimation")
|
||||
|
||||
//- XXX We should use "editable" to avoid repetiting ourselves
|
||||
template(name="cardFieldSectionDescription")
|
||||
.card-details-group.card-details-group-description
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
@@ -748,72 +842,6 @@ template(name="cardDetails")
|
||||
+viewer
|
||||
= getDescription
|
||||
|
||||
.card-checklist-attachmentGalleries
|
||||
.card-checklist-attachmentGallery.card-checklists
|
||||
if currentBoard.allowsChecklists
|
||||
+cardSectionHeader(section="checklists" icon="fa-check" label="checklists")
|
||||
if isSectionOpen "checklists"
|
||||
//- Total checklist item count (finished/total) shown above all
|
||||
checklists. The board flag is a cheap boolean, so when it is off
|
||||
the checklist counting below is never evaluated.
|
||||
if currentBoard.allowsChecklistCountBadgeOnCard
|
||||
if checklists.length
|
||||
h4.card-details-item-subtitle
|
||||
| {{checklistFinishedCount}}/{{checklistItemCount}}
|
||||
+checklists(cardId = _id card = this)
|
||||
if currentBoard.allowsSubtasks
|
||||
+cardSectionHeader(section="subtasks" icon="fa-sitemap" label="subtasks"
|
||||
menuClass="js-open-subtasks-settings" menuTitle="subtask-settings")
|
||||
if isSectionOpen "subtasks"
|
||||
+subtasks(cardId = _id)
|
||||
if currentBoard.allowsAttachments
|
||||
+cardSectionHeader(section="attachments" icon="fa-paperclip" label="attachments"
|
||||
count=attachmentCount)
|
||||
if isSectionOpen "attachments"
|
||||
if Meteor.settings.public.attachmentsUploadMaxSize
|
||||
| {{_ 'max-upload-filesize'}} {{Meteor.settings.public.attachmentsUploadMaxSize}}
|
||||
br
|
||||
if Meteor.settings.public.attachmentsUploadMimeTypes
|
||||
| {{_ 'allowed-upload-filetypes'}} {{Meteor.settings.public.attachmentsUploadMimeTypes}}
|
||||
br
|
||||
| {{_ 'invalid-file'}}
|
||||
.card-checklist-attachmentGallery.card-attachmentGallery
|
||||
+attachmentGallery
|
||||
|
||||
if currentBoard.allowsComments
|
||||
.comment-title
|
||||
+cardSectionHeader(section="comments" icon="fa-comment-o" label="comments")
|
||||
if isSectionOpen "comments"
|
||||
if currentUser.isBoardMember
|
||||
unless currentUser.isNoComments
|
||||
unless currentUser.isReadOnly
|
||||
unless currentUser.isReadAssignedOnly
|
||||
+commentForm
|
||||
+comments
|
||||
|
||||
.card-details-right
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
//- OUTSIDE .activity-title, which is `display: flex`: a rule inside it
|
||||
becomes a flex item and renders as a short line beside the heading
|
||||
instead of a separator above it.
|
||||
The eye that used to sit here showed and hid exactly what the caret
|
||||
shows and hides, so there were two controls for one thing and they
|
||||
could disagree. The caret is the one that stayed, and this section is
|
||||
the one that starts CLOSED - a card is opened to read the card, not
|
||||
its history.
|
||||
+cardSectionHeader(section="activities" icon="fa-history" label="activities")
|
||||
|
||||
if currentUser.isBoardAdmin
|
||||
if isSectionOpen "activities"
|
||||
if isLoaded.get
|
||||
if isLinkedCard
|
||||
+activities(card=this mode="linkedcard")
|
||||
else if isLinkedBoard
|
||||
+activities(card=this mode="linkedboard")
|
||||
else
|
||||
+activities(card=this mode="card")
|
||||
|
||||
//- #1173-adjacent: ONE header for every collapsible section of a card - the
|
||||
rule above it, the caret that collapses it, its icon and its name. Written
|
||||
once, so eleven sections cannot end up with ten separators and nine carets.
|
||||
@@ -1083,10 +1111,6 @@ template(name="cardDetailsActionsPopup")
|
||||
span.emoji-icon
|
||||
i.fa.fa-link
|
||||
| {{_ 'cardMorePopup-title'}}
|
||||
li
|
||||
a.js-create-board-from-card
|
||||
i.fa.fa-external-link
|
||||
| {{_ 'linkCardToNewBoard'}}
|
||||
|
||||
//- #1173: the card export popup IS the shared export body - the same checkbox
|
||||
list and the same four formats every other menu offers, with the card as its
|
||||
|
||||
@@ -606,6 +606,19 @@ Template.cardDetails.helpers({
|
||||
const board = this?.board?.();
|
||||
return Utils.canModifyCard(this) && board?.allowsCustomFields !== false;
|
||||
},
|
||||
|
||||
// #4448: the order the reorderable card-detail sections (Labels, Dates,
|
||||
// Members, Custom Fields, Description) render in, resolved from the
|
||||
// board's stored setting. models/lib/cardFieldOrder.js
|
||||
orderedCardFieldSections() {
|
||||
const board = this?.board?.();
|
||||
if (board && typeof board.getCardFieldOrder === 'function') {
|
||||
return board.getCardFieldOrder();
|
||||
}
|
||||
const { applyCardFieldOrder } = require('/models/lib/cardFieldOrder');
|
||||
return applyCardFieldOrder(board?.cardFieldOrder);
|
||||
},
|
||||
|
||||
stickers() {
|
||||
const card = Template.currentData();
|
||||
return card && typeof card.getStickers === 'function' ? card.getStickers() : [];
|
||||
|
||||
@@ -460,19 +460,37 @@ template(name="boardCardSettingsPopup")
|
||||
span
|
||||
i.fa.fa-tag
|
||||
| {{_ 'labels'}}
|
||||
//- "Labels text": the labels on a minicard as coloured WORDS, or as
|
||||
//- #4256: "Labels text" - the labels on a minicard as coloured WORDS, or as
|
||||
coloured bars with the words left out. Directly under Labels, because it
|
||||
is about what those labels look like.
|
||||
It is the one PERSONAL row of this table - the user's own profile, or this
|
||||
This BOARD row is the board's own default - a board admin sets it here,
|
||||
the same as every other row in this table, and it applies to everybody
|
||||
who has not overridden it below. Checked by default: the text is what a
|
||||
minicard shows unless an admin turns it off. Minicard-only, so the Card
|
||||
column is left blank. client/lib/minicardLabelText.js
|
||||
.card-settings-row
|
||||
.card-settings-column
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-label-text(title="{{_ 'labels'}} {{_ 'custom-field-text'}}" class="{{#if allowsLabelText}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if allowsLabelText}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
i.fa.fa-tag
|
||||
i.fa.fa-file-text-o
|
||||
| {{_ 'labels'}}
|
||||
| {{_ 'custom-field-text'}}
|
||||
//- The one PERSONAL row of this table - the user's own profile, or this
|
||||
browser when nobody is logged in - so it is shown only in the minicard's
|
||||
own view (Minicard menu / Show on Minicard) and not in the board-wide
|
||||
Card Settings, where every other row belongs to the board. Checked by
|
||||
default: the text is what a minicard shows unless somebody turns it off.
|
||||
client/lib/minicardLabelText.js
|
||||
Card Settings, where every other row (including the board default above)
|
||||
belongs to the board. It OVERRIDES the board default above: checking or
|
||||
unchecking it here always wins, on every board, until reset back to
|
||||
"follow the board" - mirroring the global theme override
|
||||
(client/components/main/globalThemeColor.js). client/lib/minicardLabelText.js
|
||||
.card-settings-row.card-settings-row-personal
|
||||
.card-settings-column
|
||||
.card-settings-column
|
||||
a.flex.js-toggle-minicard-label-text(title="{{_ 'labels'}} {{_ 'custom-field-text'}}" class="{{#if showsMinicardLabelText}}is-checked{{/if}}")
|
||||
a.flex.js-toggle-minicard-label-text(title="{{_ 'labels'}} {{_ 'custom-field-text'}} {{_ 'label-text-override'}}" class="{{#if showsMinicardLabelText}}is-checked{{/if}}")
|
||||
i.fa(class="{{#if showsMinicardLabelText}}fa-check{{else}}fa-square-o{{/if}}")
|
||||
.card-settings-column
|
||||
span
|
||||
@@ -480,6 +498,12 @@ template(name="boardCardSettingsPopup")
|
||||
i.fa.fa-file-text-o
|
||||
| {{_ 'labels'}}
|
||||
| {{_ 'custom-field-text'}}
|
||||
if hasLabelTextOverride
|
||||
|
|
||||
a.js-reset-minicard-label-text-override(title="{{_ 'label-text-use-board-default'}}")
|
||||
| ({{_ 'label-text-overridden'}})
|
||||
else
|
||||
| ({{_ 'label-text-follows-board'}})
|
||||
.card-settings-row
|
||||
.card-settings-column
|
||||
a.flex.js-field-has-custom-fields(title="{{_ 'custom-fields'}}" class="{{#if allowsCustomFields}}is-checked{{/if}}")
|
||||
@@ -627,6 +651,29 @@ template(name="boardCardSettingsPopup")
|
||||
//
|
||||
| {{_ 'activities'}}
|
||||
|
||||
//- #4448: reorder the major sections of the opened card (Labels, Dates,
|
||||
Members, Custom Fields, Description) - move Description earlier and have
|
||||
Custom Fields render after it, instead of the historical fixed order.
|
||||
Up/down buttons rather than a drag library: no jQuery-ui-sortable-based
|
||||
reorder of a plain settings LIST exists elsewhere in this codebase to
|
||||
reuse (the sortable usages for lists/swimlanes/boards are heavier
|
||||
drag-and-drop over board layout, not a simple ordered list), so buttons
|
||||
are the simpler, safer mechanism here.
|
||||
client/components/sidebar/sidebar.js, models/lib/cardFieldOrder.js
|
||||
if canModifyBoard
|
||||
.card-settings-row
|
||||
.card-settings-column
|
||||
h4 {{_ 'card-field-order'}}
|
||||
each cardFieldOrderRows
|
||||
.card-settings-row.js-card-field-order-row(data-key="{{key}}")
|
||||
.card-settings-column
|
||||
a.flex.js-card-field-order-up(class="{{#if isFirst}}is-disabled{{/if}}" title="{{_ 'card-field-order-move-up'}}")
|
||||
i.fa.fa-arrow-up
|
||||
a.flex.js-card-field-order-down(class="{{#if isLast}}is-disabled{{/if}}" title="{{_ 'card-field-order-move-down'}}")
|
||||
i.fa.fa-arrow-down
|
||||
.card-settings-column
|
||||
span {{_ label}}
|
||||
|
||||
//- The board's Subtasks Settings. Its own template, because the same settings
|
||||
are read from two places: Board Settings, and the menu of a subtask on an
|
||||
opened card - which is where somebody looking at a subtask is when they want
|
||||
|
||||
@@ -9,6 +9,10 @@ const {
|
||||
SIDEBAR_BACK_CLOSE,
|
||||
sidebarBackAction,
|
||||
} = require('/models/lib/sidebarBackAction');
|
||||
const {
|
||||
applyCardFieldOrder,
|
||||
moveCardFieldKey,
|
||||
} = require('/models/lib/cardFieldOrder');
|
||||
import { InfiniteScrolling } from '/client/lib/infiniteScrolling';
|
||||
import '/client/components/boards/exportScope';
|
||||
import AccessibilitySettings from '/models/accessibilitySettings';
|
||||
@@ -30,7 +34,7 @@ import { caretClassFor } from '/client/lib/sectionCaret';
|
||||
import { buildAttachmentUploadConfig } from '/client/lib/attachmentUploadConfig';
|
||||
import { toggleFold } from '/client/lib/foldState';
|
||||
import {
|
||||
hiddenMinicardLabelText,
|
||||
resolveShowLabelText,
|
||||
toggleMinicardLabelText,
|
||||
} from '/client/lib/minicardLabelText';
|
||||
import {
|
||||
@@ -1633,12 +1637,55 @@ Template.boardCardSettingsPopup.helpers({
|
||||
return classes.join(' ');
|
||||
},
|
||||
|
||||
// Checked means the labels on a minicard show their TEXT, which is what they
|
||||
// do unless somebody turns it off - so the stored "hidden" flag is read the
|
||||
// other way round here. A checkbox that is unticked by default for the
|
||||
// default behaviour reads as broken. client/lib/minicardLabelText.js
|
||||
// #4448: the reorderable card-detail sections (Labels, Dates, Members,
|
||||
// Custom Fields, Description), in the board's current order, each with
|
||||
// whether it is first/last so the up/down buttons can disable themselves at
|
||||
// the ends. models/lib/cardFieldOrder.js
|
||||
cardFieldOrderRows() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
const order = applyCardFieldOrder(currentBoard?.cardFieldOrder);
|
||||
const labelForKey = {
|
||||
labels: 'labels',
|
||||
dates: 'date-format',
|
||||
members: 'members',
|
||||
customFields: 'custom-fields',
|
||||
description: 'description',
|
||||
};
|
||||
return order.map((key, index) => ({
|
||||
key,
|
||||
label: labelForKey[key] || key,
|
||||
isFirst: index === 0,
|
||||
isLast: index === order.length - 1,
|
||||
}));
|
||||
},
|
||||
|
||||
// Board-level DEFAULT (#4256, Board Settings): whether this board shows
|
||||
// label text on its minicards unless a user overrides it. Checked means
|
||||
// shown, which is what a board does unless an admin turns it off.
|
||||
allowsLabelText() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return currentBoard ? currentBoard.showLabelText !== false : true;
|
||||
},
|
||||
|
||||
// The user's own PERSONAL override on top of the board's default, tri-state:
|
||||
// no override (follow the board), forced shown, or forced hidden. Checked
|
||||
// means the resolved value is "shown" - so the checkbox always reflects what
|
||||
// the minicard is currently doing, not just whether an override is set.
|
||||
// client/lib/minicardLabelText.js
|
||||
showsMinicardLabelText() {
|
||||
return !hiddenMinicardLabelText();
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
return resolveShowLabelText(currentBoard);
|
||||
},
|
||||
|
||||
// Whether the user has an explicit override set at all, so the template can
|
||||
// show "(follows board default)" vs. "(overridden)".
|
||||
hasLabelTextOverride() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (!currentUser) return false;
|
||||
return typeof (currentUser.profile || {}).showLabelTextOverride === 'boolean';
|
||||
},
|
||||
|
||||
// "List title" is the CARD's own setting, not the board's, so the card is
|
||||
@@ -1953,11 +2000,47 @@ Template.boardCardSettingsPopup.helpers({
|
||||
});
|
||||
|
||||
Template.boardCardSettingsPopup.events({
|
||||
// #4448: Board Settings / Card Settings up/down reorder of the card-detail
|
||||
// sections. Re-reads the board's CURRENT order on every click (rather than
|
||||
// trusting the row's stale data context) so two quick clicks in a row each
|
||||
// move from where the previous one actually left it.
|
||||
'click .js-card-field-order-up'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
if (!currentBoard) return;
|
||||
const key = this.key;
|
||||
const newOrder = moveCardFieldKey(currentBoard.cardFieldOrder, key, 'up');
|
||||
Boards.update(currentBoard._id, { $set: { cardFieldOrder: newOrder } });
|
||||
},
|
||||
'click .js-card-field-order-down'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const boardId = Session.get('currentBoard');
|
||||
const currentBoard = ReactiveCache.getBoard(boardId);
|
||||
if (!currentBoard) return;
|
||||
const key = this.key;
|
||||
const newOrder = moveCardFieldKey(currentBoard.cardFieldOrder, key, 'down');
|
||||
Boards.update(currentBoard._id, { $set: { cardFieldOrder: newOrder } });
|
||||
},
|
||||
// Board-level default for #4256: whether labels show their TEXT on this
|
||||
// board's minicards, unless a user's own override (below) says otherwise.
|
||||
'click .js-field-has-label-text'(evt, tpl) {
|
||||
evt.preventDefault();
|
||||
const newValue = !(tpl.currentBoard.showLabelText !== false);
|
||||
Boards.update(tpl.currentBoard._id, { $set: { showLabelText: newValue } });
|
||||
},
|
||||
// The one row of this table that is the user's own, not the board's.
|
||||
'click .js-toggle-minicard-label-text'(evt) {
|
||||
evt.preventDefault();
|
||||
toggleMinicardLabelText();
|
||||
},
|
||||
// Explicit "back to the board's own setting" for the per-user override,
|
||||
// rather than making somebody cycle the checkbox through both states again.
|
||||
'click .js-reset-minicard-label-text-override'(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
Meteor.call('setShowLabelTextOverride', null);
|
||||
},
|
||||
// ...and the one that is this CARD's. The board-wide "Show lists" row further
|
||||
// down turns the list name on for every card; this turns it on for one.
|
||||
'click .js-toggle-show-list-on-minicard'(evt) {
|
||||
|
||||
@@ -677,6 +677,22 @@ Boards.attachSchema(
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
// #4448: the order the major sections of the opened card (Labels, Dates,
|
||||
// Members, Custom Fields, Description) render in. A missing/partial/unknown
|
||||
// value falls back to the historical fixed order via
|
||||
// applyCardFieldOrder() in models/lib/cardFieldOrder.js - see that file for
|
||||
// which sections are (and are not) covered by this setting.
|
||||
cardFieldOrder: {
|
||||
/**
|
||||
* The order of the reorderable card-detail-view sections
|
||||
*/
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'cardFieldOrder.$': {
|
||||
type: String,
|
||||
},
|
||||
|
||||
allowsCoverAttachmentOnMinicard: {
|
||||
/**
|
||||
* Does the board allows cover attachment on minicard?
|
||||
@@ -783,6 +799,20 @@ Boards.attachSchema(
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
showLabelText: {
|
||||
/**
|
||||
* #4256: does this board show label TEXT on its minicards (coloured
|
||||
* words), or only the coloured bars? Board Settings / Card ("Labels
|
||||
* text"). A user's own profile.showLabelTextOverride, when set, wins
|
||||
* over this on every board - see client/lib/minicardLabelText.js.
|
||||
* Defaults to true (text shown), the historical global default, so an
|
||||
* existing board with no value stored sees no behaviour change.
|
||||
*/
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
optional: true,
|
||||
},
|
||||
|
||||
allowsAssignee: {
|
||||
/**
|
||||
* Does the board allows assignee?
|
||||
@@ -1635,6 +1665,13 @@ Boards.helpers({
|
||||
return ret;
|
||||
},
|
||||
|
||||
// #4448: the resolved, always-complete order of the reorderable card-detail
|
||||
// sections - see models/lib/cardFieldOrder.js.
|
||||
getCardFieldOrder() {
|
||||
const { applyCardFieldOrder } = require('/models/lib/cardFieldOrder');
|
||||
return applyCardFieldOrder(this.cardFieldOrder);
|
||||
},
|
||||
|
||||
absoluteUrl() {
|
||||
// Build the URL from the relative path rather than FlowRouter.url():
|
||||
// FlowRouter is client-only, so on the server (board invitation emails,
|
||||
@@ -2396,6 +2433,15 @@ Boards.helpers({
|
||||
return await this.setAllowsCardSortingByNumberOnMinicard(allowsCardSortingByNumberOnMinicard);
|
||||
},
|
||||
|
||||
// #4256: this board's own default for showing label text on minicards.
|
||||
getShowLabelText() {
|
||||
return this.showLabelText !== false;
|
||||
},
|
||||
|
||||
async setShowLabelText(showLabelText) {
|
||||
return await Boards.updateAsync(this._id, { $set: { showLabelText } });
|
||||
},
|
||||
|
||||
async setAllowsActivities(allowsActivities) {
|
||||
return await Boards.updateAsync(this._id, { $set: { allowsActivities } });
|
||||
},
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
// Pure helpers for the card-detail-view "field order" setting (#4448): a board
|
||||
// may reorder the major sections of the opened card - move Description earlier
|
||||
// (e.g. third) and have Custom Fields render right after it, rather than the
|
||||
// previously fixed sequence. Extracted so the ordering decision is
|
||||
// unit-testable in plain Node without Meteor (mirrors models/lib/boardSortReorder.js).
|
||||
//
|
||||
// Scope: only the sections that already share the simple `.card-details-group`
|
||||
// wrapper in client/components/cards/cardDetails.jade are reorderable - Labels,
|
||||
// Dates, Members, Custom Fields and Description. Dependencies/Sort (rendered as
|
||||
// a fixed appendage right after Members), Vote/Poker (a fixed appendage right
|
||||
// after Custom Fields), and Checklists/Attachments/Comments/Activity (which sit
|
||||
// in their own flex/right-column layout further down the template) are left at
|
||||
// their current, unreorderable positions to avoid layout regressions that
|
||||
// cannot be verified visually in this environment.
|
||||
|
||||
// The default order also documents the ORIGINAL fixed sequence, so a board that
|
||||
// never touches the setting renders byte-for-byte what it always has.
|
||||
const DEFAULT_CARD_FIELD_ORDER = [
|
||||
'labels',
|
||||
'dates',
|
||||
'members',
|
||||
'customFields',
|
||||
'description',
|
||||
];
|
||||
|
||||
// Keep in sync with DEFAULT_CARD_FIELD_ORDER's members - the set of valid keys.
|
||||
const CARD_FIELD_ORDER_KEYS = DEFAULT_CARD_FIELD_ORDER.slice();
|
||||
|
||||
function isValidCardFieldKey(key) {
|
||||
return CARD_FIELD_ORDER_KEYS.includes(key);
|
||||
}
|
||||
|
||||
// Turn a (possibly stale, partial, unknown-containing, or empty) stored order
|
||||
// into a complete, valid ordering: unknown keys are dropped, duplicates are
|
||||
// dropped (keeping the first occurrence), and any known key missing from the
|
||||
// stored order is appended at the end, in its default-order relative position.
|
||||
// Never throws and never drops a known section - the card must always render
|
||||
// every section exactly once.
|
||||
function applyCardFieldOrder(storedOrder, defaultOrder = DEFAULT_CARD_FIELD_ORDER) {
|
||||
const known = defaultOrder.slice();
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
|
||||
if (Array.isArray(storedOrder)) {
|
||||
storedOrder.forEach(key => {
|
||||
if (typeof key === 'string' && known.includes(key) && !seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
known.forEach(key => {
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Move the section at `key` one step earlier/later in `order`, used by the
|
||||
// Board Settings up/down reorder buttons. Returns a new array; a no-op (key
|
||||
// missing, or already at that end) returns the same values (by value) as
|
||||
// `order` unchanged.
|
||||
function moveCardFieldKey(order, key, direction) {
|
||||
const list = applyCardFieldOrder(order);
|
||||
const from = list.indexOf(key);
|
||||
if (from === -1) return list;
|
||||
const to = direction === 'up' ? from - 1 : from + 1;
|
||||
if (to < 0 || to >= list.length) return list;
|
||||
const result = list.slice();
|
||||
const [moved] = result.splice(from, 1);
|
||||
result.splice(to, 0, moved);
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_CARD_FIELD_ORDER,
|
||||
CARD_FIELD_ORDER_KEYS,
|
||||
isValidCardFieldKey,
|
||||
applyCardFieldOrder,
|
||||
moveCardFieldKey,
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
'use strict';
|
||||
|
||||
// Unit test for the card-detail-view "field order" setting (#4448): a board can
|
||||
// reorder the major sections of the opened card - move Description earlier
|
||||
// (e.g. third) and have Custom Fields render right after it. Run:
|
||||
// node tests/cardFieldOrder.test.cjs
|
||||
//
|
||||
// The pure decision under test is `applyCardFieldOrder()`, which turns a
|
||||
// (possibly missing/partial/unknown-containing) stored order into a complete,
|
||||
// valid ordering that always contains every known section exactly once - so
|
||||
// the card can never render a section twice or drop one because of stale or
|
||||
// corrupted board data.
|
||||
|
||||
const assert = require('assert');
|
||||
const {
|
||||
DEFAULT_CARD_FIELD_ORDER,
|
||||
CARD_FIELD_ORDER_KEYS,
|
||||
isValidCardFieldKey,
|
||||
applyCardFieldOrder,
|
||||
moveCardFieldKey,
|
||||
} = require('../models/lib/cardFieldOrder');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
// --- default / fallback behaviour -------------------------------------------
|
||||
test('no stored order at all falls back to the historical fixed order', () => {
|
||||
assert.deepStrictEqual(applyCardFieldOrder(undefined), DEFAULT_CARD_FIELD_ORDER);
|
||||
assert.deepStrictEqual(applyCardFieldOrder(null), DEFAULT_CARD_FIELD_ORDER);
|
||||
assert.deepStrictEqual(applyCardFieldOrder([]), DEFAULT_CARD_FIELD_ORDER);
|
||||
});
|
||||
|
||||
test('a non-array stored order falls back to the default order', () => {
|
||||
assert.deepStrictEqual(applyCardFieldOrder('description'), DEFAULT_CARD_FIELD_ORDER);
|
||||
assert.deepStrictEqual(applyCardFieldOrder({}), DEFAULT_CARD_FIELD_ORDER);
|
||||
});
|
||||
|
||||
// --- the actual issue: move description earlier, custom fields after it ----
|
||||
test('#4448: description can be moved to third, with custom fields right after it', () => {
|
||||
const stored = ['labels', 'dates', 'description', 'customFields', 'members'];
|
||||
assert.deepStrictEqual(applyCardFieldOrder(stored), stored);
|
||||
const order = applyCardFieldOrder(stored);
|
||||
assert.strictEqual(order.indexOf('description'), 2, 'description is third (0-indexed 2)');
|
||||
assert.ok(
|
||||
order.indexOf('customFields') > order.indexOf('description'),
|
||||
'custom fields render after description',
|
||||
);
|
||||
});
|
||||
|
||||
// --- robustness: unknown / duplicate / partial values -----------------------
|
||||
test('unknown keys in the stored order are dropped', () => {
|
||||
const order = applyCardFieldOrder(['labels', 'bogus-section', 'dates']);
|
||||
assert.ok(!order.includes('bogus-section'));
|
||||
assert.strictEqual(order.length, DEFAULT_CARD_FIELD_ORDER.length);
|
||||
});
|
||||
|
||||
test('duplicate keys keep only the first occurrence', () => {
|
||||
const order = applyCardFieldOrder(['description', 'description', 'labels']);
|
||||
assert.strictEqual(order.filter(k => k === 'description').length, 1);
|
||||
});
|
||||
|
||||
test('a partial stored order is completed with the missing known keys, appended in default order', () => {
|
||||
const order = applyCardFieldOrder(['description', 'labels']);
|
||||
assert.deepStrictEqual(order.slice(0, 2), ['description', 'labels']);
|
||||
// The remaining keys (dates, members, customFields) follow, in their
|
||||
// DEFAULT_CARD_FIELD_ORDER relative order.
|
||||
const remaining = DEFAULT_CARD_FIELD_ORDER.filter(k => k !== 'description' && k !== 'labels');
|
||||
assert.deepStrictEqual(order.slice(2), remaining);
|
||||
});
|
||||
|
||||
test('every known section is present exactly once, regardless of input', () => {
|
||||
const inputs = [
|
||||
undefined,
|
||||
[],
|
||||
['description'],
|
||||
['bogus', 'bogus', 'description', 'description'],
|
||||
DEFAULT_CARD_FIELD_ORDER.slice().reverse(),
|
||||
];
|
||||
inputs.forEach(input => {
|
||||
const order = applyCardFieldOrder(input);
|
||||
assert.strictEqual(order.length, CARD_FIELD_ORDER_KEYS.length);
|
||||
CARD_FIELD_ORDER_KEYS.forEach(key => {
|
||||
assert.strictEqual(order.filter(k => k === key).length, 1, `key ${key} appears exactly once`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- isValidCardFieldKey -----------------------------------------------------
|
||||
test('isValidCardFieldKey recognises only the known section keys', () => {
|
||||
assert.strictEqual(isValidCardFieldKey('description'), true);
|
||||
assert.strictEqual(isValidCardFieldKey('customFields'), true);
|
||||
assert.strictEqual(isValidCardFieldKey('checklists'), false);
|
||||
assert.strictEqual(isValidCardFieldKey(''), false);
|
||||
assert.strictEqual(isValidCardFieldKey(undefined), false);
|
||||
});
|
||||
|
||||
// --- moveCardFieldKey (Board Settings up/down buttons) ----------------------
|
||||
test('moveCardFieldKey moves a section one step earlier', () => {
|
||||
const order = moveCardFieldKey(DEFAULT_CARD_FIELD_ORDER, 'description', 'up');
|
||||
assert.strictEqual(order.indexOf('description'), DEFAULT_CARD_FIELD_ORDER.indexOf('description') - 1);
|
||||
});
|
||||
|
||||
test('moveCardFieldKey moves a section one step later', () => {
|
||||
const order = moveCardFieldKey(DEFAULT_CARD_FIELD_ORDER, 'labels', 'down');
|
||||
assert.strictEqual(order.indexOf('labels'), 1);
|
||||
});
|
||||
|
||||
test('moveCardFieldKey at the top boundary is a no-op going up', () => {
|
||||
const order = moveCardFieldKey(DEFAULT_CARD_FIELD_ORDER, 'labels', 'up');
|
||||
assert.deepStrictEqual(order, DEFAULT_CARD_FIELD_ORDER);
|
||||
});
|
||||
|
||||
test('moveCardFieldKey at the bottom boundary is a no-op going down', () => {
|
||||
const order = moveCardFieldKey(DEFAULT_CARD_FIELD_ORDER, 'description', 'down');
|
||||
assert.deepStrictEqual(order, DEFAULT_CARD_FIELD_ORDER);
|
||||
});
|
||||
|
||||
test('moveCardFieldKey with an unknown key is a no-op', () => {
|
||||
const order = moveCardFieldKey(DEFAULT_CARD_FIELD_ORDER, 'bogus', 'up');
|
||||
assert.deepStrictEqual(order, DEFAULT_CARD_FIELD_ORDER);
|
||||
});
|
||||
|
||||
// --- NEGATIVE test: the jade template must not still hardcode the old fixed
|
||||
// sequence for these sections - it renders from orderedCardFieldSections().
|
||||
test('cardDetails.jade renders the reorderable sections from an order-driven loop, not a hardcoded sequence', () => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const jade = fs.readFileSync(
|
||||
path.join(__dirname, '../client/components/cards/cardDetails.jade'),
|
||||
'utf8',
|
||||
);
|
||||
assert.ok(
|
||||
jade.includes('each orderedCardFieldSections'),
|
||||
'the card-details-items block must iterate the resolved order',
|
||||
);
|
||||
// Each reorderable section must be reachable only through its own named
|
||||
// template (called from the order loop), not inlined at a fixed spot.
|
||||
CARD_FIELD_ORDER_KEYS.forEach(key => {
|
||||
const templateName = `cardFieldSection${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
||||
assert.ok(
|
||||
jade.includes(`template(name="${templateName}")`),
|
||||
`${templateName} must be its own template`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\n${passed} tests passed`);
|
||||
Reference in new issue
Block a user