Card Settings: Select all and Select none per column, headings aligned with their cells, labels whole.

Under each column heading of every pane - On this card, Show on
Minicard, Show on Card, Show empty - Select all and Select none set
that column for the open section's rows at once, each the way its own
checkbox would: On this card in one write of the card, Show empty in
one write of the board's list, the Scrum rows in one Scrum write (one
by one, each would carry the same revision), the others through their
own checkbox's handler. Who may change the column gets the buttons; a
personal row (Labels text, List title) is left as it is.

The headings drifted left of their cells, further each column, and the
labels were cut: the heading line had the smaller font, and the columns
are in em, so its columns were narrower than the rows'. The font is on
the headings now, every heading and cell starts at its column's start,
and the popup is up to 1200px wide instead of 900px, so a row's whole
label fits where the window has the room.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu committed 2026-10-26 16:40:14 +02:00
1 parent c687fe5577
commit c8a15b514f
14 files changed
+308 -24

No files matched your search

+1 -1
View File
@@ -379,7 +379,7 @@
.pop-over[data-popup='boardCardSettingsPopup'] {
position: fixed;
box-sizing: border-box;
width: min(96vw, 900px) !important;
width: min(96vw, 1200px) !important;
max-width: 96vw !important;
max-height: calc(100dvh - 24px) !important;
margin-top: 0;
+34 -1
View File
@@ -179,6 +179,10 @@
grid-template-columns: 9em 9em 8em minmax(10em, 1fr);
column-gap: 14px;
align-items: center;
/* Every heading and every cell starts at its column's left edge (its start,
right-to-left), so a heading and the checkboxes under it line up. */
justify-items: start;
text-align: start;
}
.board-card-settings.has-card .card-settings-pane-head,
.board-card-settings.has-card .card-settings-line {
@@ -186,9 +190,16 @@
}
.card-settings-pane-head {
font-weight: bold;
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
border-bottom: 1px solid #eee;
padding-block-end: 4px;
align-items: start;
}
/* The headings' smaller font is on the headings, not on their line: the
columns are in em, and a smaller font on the line made every column of the
heading narrower than the same column of the rows - each heading drifted
further left of its checkboxes, and the rows' labels past the pane. */
.card-settings-pane-head > span {
font-size: calc(12px * var(--wekan-ui-font-scale, 1));
}
.card-settings-line {
padding: calc(4px * min(1, var(--wekan-ui-font-scale, 1))) 0;
@@ -197,6 +208,9 @@
.card-settings-side-head {
/* A heading longer than its column, in another language, wraps in it. */
overflow-wrap: anywhere;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.card-settings-line-label {
display: inline-flex;
@@ -208,8 +222,27 @@
.card-settings-side {
display: inline-flex;
align-items: center;
justify-content: flex-start;
justify-self: start;
gap: 6px;
}
.card-settings-side > a.flex {
margin: 0;
}
/* Select all / Select none under a column's heading, for that column's rows
of the open section. */
.card-settings-select {
display: flex;
flex-wrap: wrap;
gap: 2px 6px;
margin-block-start: 3px;
font-weight: normal;
}
.card-settings-select-button {
white-space: nowrap;
text-decoration: underline;
cursor: pointer;
}
.card-settings-position {
min-width: 1.6em;
text-align: end;
+25 -4
View File
@@ -350,12 +350,25 @@ template(name="boardCardSettingsPopup")
//- Reading order: On this card (opened from a card), Show on
Minicard, Show on Card, then the field - mirrored in a
right-to-left language by the grid itself.
//- Under each heading, Select all / Select none for that column's
rows of this section - for who may change what it sets.
.card-settings-pane-head
if settingsCardId
span.card-settings-side-head.card-settings-side-head-this {{_ 'card-settings-this-card'}}
span.card-settings-side-head.card-settings-side-head-minicard {{_ 'show-on-minicard'}}
span.card-settings-side-head.card-settings-side-head-card {{_ 'show-on-card'}}
span.card-settings-side-head.card-settings-side-head-always {{_ 'card-settings-show-empty'}}
span.card-settings-side-head.card-settings-side-head-this
span {{_ 'card-settings-this-card'}}
+cardSettingsSelectButtons(column="this")
span.card-settings-side-head.card-settings-side-head-minicard
span {{_ 'show-on-minicard'}}
if canModifyBoard
+cardSettingsSelectButtons(column="minicard")
span.card-settings-side-head.card-settings-side-head-card
span {{_ 'show-on-card'}}
if canModifyBoard
+cardSettingsSelectButtons(column="card")
span.card-settings-side-head.card-settings-side-head-always
span {{_ 'card-settings-show-empty'}}
if canModifyBoard
+cardSettingsSelectButtons(column="always")
span.card-settings-pane-head-label {{_ 'card-field-order'}}
each row in sectionSettingsRows
.card-settings-line(class="{{#if row.personal}}card-settings-row-personal{{/if}}" data-key="{{row.key}}")
@@ -414,6 +427,14 @@ template(name="boardCardSettingsPopup")
|  ({{_ 'label-text-follows-board'}})
//- Select all / Select none of one column of Card Settings' pane
(boardCardSettingsPopup's 'click .js-card-settings-select').
template(name="cardSettingsSelectButtons")
span.card-settings-select
a.card-settings-select-button.js-card-settings-select(data-column="{{column}}" data-value="all" role="button" tabindex="0") {{_ 'select-all'}}
a.card-settings-select-button.js-card-settings-select(data-column="{{column}}" data-value="none" role="button" tabindex="0") {{_ 'select-none'}}
//- 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
+65
View File
@@ -2786,6 +2786,71 @@ Template.boardCardSettingsPopup.events({
const next = cardShownFields.cardFieldsAfter(card, key, !cardShownFields.isShown(key, card, counts, board), counts, board);
await Cards.updateAsync(card._id, { $set: next });
},
// Select all / Select none under a column's heading: that column's rows of
// the open section (the ones drawn in the pane), each set the way its own
// checkbox would set it - the board's columns for a board admin, On this
// card for who may change the card. A personal row (Labels text, List
// title) is the user's or the card's own, not the board's: left as it is.
async 'click .js-card-settings-select'(evt, tpl) {
evt.preventDefault();
const { column, value } = evt.currentTarget.dataset;
const on = value === 'all';
const pane = evt.currentTarget.closest('.card-settings-pane');
const board = ReactiveCache.getBoard(Session.get('currentBoard'));
if (!pane || !board) return;
const isOn = el => el.getAttribute('aria-checked') === 'true';
const keysOf = selector => [...pane.querySelectorAll(selector)].map(el => el.dataset.key).filter(Boolean);
tpl.animating.set(null);
try {
if (column === 'this') {
const card = settingsCard();
if (!card || !Utils.canModifyCard(card)) return;
const counts = cardFieldCounts(card);
let next = { shownFields: card.shownFields || [], hiddenFields: card.hiddenFields || [] };
keysOf('.js-card-add-field').forEach(key => {
next = cardShownFields.cardFieldsAfter({ ...card, ...next }, key, on, counts, board);
});
await Cards.updateAsync(card._id, { $set: next });
return;
}
if (!(Utils.canModifyBoard && Utils.canModifyBoard())) return;
if (column === 'always') {
await board.setCardFieldAlwaysShown(keysOf('.js-card-always-show'), on);
return;
}
if (column !== 'minicard' && column !== 'card') return;
const cells = [...pane.querySelectorAll(`.js-card-field-order-row[data-side="${column}"]`)]
.filter(cell => !cell.classList.contains('card-settings-row-personal'))
.map(cell => cell.querySelector('.card-field-order-toggle'))
.filter(toggle => toggle && !toggle.classList.contains('is-disabled') && isOn(toggle) !== on);
// The Scrum rows are one write of the board's Scrum visibility: one by
// one, each would carry the same revision and all but the first fail.
const scrum = {};
cells.filter(t => t.classList.contains('js-scrum-field-toggle')).forEach(toggle => {
const key = toggle.closest('.js-card-field-order-row').dataset.key;
const spec = CARD_SETTINGS_ROWS.find(r => r.key === key)?.[column];
if (spec?.scrum) scrum[spec.scrum] = on;
});
if (Object.keys(scrum).length) {
await Meteor.callAsync('scrum.configure', board._id, { visibility: scrum }, board.scrumRevision || 0);
invalidateScrumNames();
}
// Every other row through its own checkbox's handler, which knows the
// field it sets and its default.
cells.filter(t => !t.classList.contains('js-scrum-field-toggle')).forEach(toggle => toggle.click());
} catch (error) {
console.error(error);
} finally {
tpl.animating.set(null);
}
},
// Keyboard: Enter or Space on a focused Select all / Select none.
'keydown .js-card-settings-select'(evt) {
if (evt.key === 'Enter' || evt.key === ' ') {
evt.preventDefault();
evt.currentTarget.click();
}
},
// Card Settings / Card's "Show empty fields": every card shows all its
// fields, the empty ones too. A board admin's, like the rest.
async 'click .js-show-empty-card-fields'(evt, tpl) {
+3 -3
View File
@@ -27,9 +27,9 @@ const MOBILE_POPUP_MAX_WIDTH = 800;
// #6796: Board Settings / Card's widest; popup.css says the same
// (`.pop-over[data-popup='boardCardSettingsPopup']`).
// A left menu and a pane of rows (docs/Features/Cards/Card-Fields.md): 900px
// is room for a row's label and both its sides.
const CARD_SETTINGS_MAX_WIDTH = 900;
// A left menu and a pane of rows (docs/Features/Cards/Card-Fields.md): 1200px
// is room for the menu, a row's four checkbox columns and its whole label.
const CARD_SETTINGS_MAX_WIDTH = 1200;
function computePopupOffset(params) {
const {
@@ -38,6 +38,9 @@ open section's rows:
| position | where the field is on that side, counted among the fields that move |
| ▲ ▼ | move the field earlier or later **on that side only** |
Under each column's heading, **Select all** and **Select none** set that
column for every row of the open section at once.
The minicard order and the card order are independent: a field can be third
on the minicard and last on the card. On a narrow screen the menu stacks above
the pane, as it does in the Admin Panel.
+8
View File
@@ -58,6 +58,14 @@ right.
description, attachments and comments are ticked.
- Opened from a card, one more cell, **On this card**, says whether this card
shows the field (the per-card fields only).
- Under each column's heading, **Select all** and **Select none** set that
column for every row of the open section at once - the board's columns for a
board admin, On this card for who may change the card. A personal row
(Labels text, List title) is left as it is.
Each heading and the cells under it start at the same place, and the popup is
up to 1200px wide - as wide as the window allows - so that where there is room
every row's whole label is shown; a label longer than its column wraps.
A row's name leaves out what its section already says: under **Scrum settings**
the rows are Sprint, Past sprints, Release, Issue type, Acceptance criteria and
+3 -2
View File
@@ -2333,8 +2333,9 @@ Boards.helpers({
return await Boards.updateAsync(this._id, { $set: { cardFieldOrder: applyCardOrder(order) } });
},
// #6806: Board Settings / Card's "Always show", normalised the same way
// (models/lib/cardShownFields.js alwaysShownAfter).
// #6806: Board Settings / Card's "Show empty", normalised the same way
// (models/lib/cardShownFields.js alwaysShownAfter). `key` may be a list:
// Select all / Select none of the column.
async setCardFieldAlwaysShown(key, show) {
const { alwaysShownAfter } = require('/models/lib/cardShownFields');
return await Boards.updateAsync(this._id, { $set: { cardFieldsAlwaysShown: alwaysShownAfter(this, key, show) } });
+7 -6
View File
@@ -133,14 +133,15 @@ function boardAlwaysShows(board, key) {
return FIELD_KEYS.includes(key) && alwaysShownOf(board).includes(key);
}
// The board's list after ticking (show = true) or unticking `key`, in
// FIELD_KEYS' order, without unknown or repeated keys.
// The board's list after ticking (show = true) or unticking `key` - or a
// list of keys, for Select all / Select none - in FIELD_KEYS' order, without
// unknown or repeated keys.
function alwaysShownAfter(board, key, show) {
const now = new Set(alwaysShownOf(board));
if (FIELD_KEYS.includes(key)) {
if (show) now.add(key);
else now.delete(key);
}
(Array.isArray(key) ? key : [key]).filter(k => FIELD_KEYS.includes(k)).forEach(k => {
if (show) now.add(k);
else now.delete(k);
});
return FIELD_KEYS.filter(k => now.has(k));
}
+4 -2
View File
@@ -252,8 +252,10 @@ test('Board Settings / Card is the Admin Panel left menu and pane, with Show on
const tpl = sidebarJade.slice(sidebarJade.indexOf('template(name="boardCardSettingsPopup")'));
const form = tpl.slice(0, tpl.indexOf('\ntemplate(name='));
assert.ok(form.includes('+leftMenu(settingsMenuItems)') && form.includes('+paneTitle(settingsPaneTitle)'));
const minicard = form.indexOf("span.card-settings-side-head.card-settings-side-head-minicard {{_ 'show-on-minicard'}}");
const card = form.indexOf("span.card-settings-side-head.card-settings-side-head-card {{_ 'show-on-card'}}");
// The heading text is its own span now, with Select all / Select none under
// it (tests/cardSettingsSelectAllAligned.test.cjs).
const minicard = form.indexOf("span.card-settings-side-head.card-settings-side-head-minicard\n span {{_ 'show-on-minicard'}}");
const card = form.indexOf("span.card-settings-side-head.card-settings-side-head-card\n span {{_ 'show-on-card'}}");
assert.ok(minicard !== -1 && card !== -1 && minicard < card, 'Show on Minicard, then Show on Card');
assert.ok(en['show-on-card'] && en['show-on-minicard'], 'existing, translated keys');
assert.ok(!form.includes('card-field-order-heading'), 'the old single heading is gone (negative)');
+2 -1
View File
@@ -120,7 +120,8 @@ test('stored on the board, through one normalising method, as a board admin', ()
test('Board Settings / Card and + Add: a Show empty column, for every per-card field', () => {
const jade = read('client/components/sidebar/sidebar.jade');
const popup = jade.slice(jade.indexOf('template(name="boardCardSettingsPopup")')).split('\ntemplate(')[0];
assert.match(popup, /span\.card-settings-side-head\.card-settings-side-head-always \{\{_ 'card-settings-show-empty'\}\}/);
// Its text is a span of its own, with Select all / Select none under it.
assert.match(popup, /span\.card-settings-side-head\.card-settings-side-head-always\n\s+span \{\{_ 'card-settings-show-empty'\}\}/);
assert.match(popup, /if row\.alwaysShowable\n\s+span\.card-settings-side\.card-settings-side-always\n\s+a\.flex\.card-always-show\(class="\{\{#if canModifyBoard\}\}js-card-always-show\{\{else\}\}is-disabled\{\{\/if\}\}/);
assert.match(popup, /role="checkbox" aria-checked="\{\{#if row\.alwaysShown\}\}true\{\{else\}\}false\{\{\/if\}\}"/);
const head = popup.slice(popup.indexOf('.card-settings-pane-head'), popup.indexOf('each row in sectionSettingsRows'));
@@ -0,0 +1,95 @@
'use strict';
// Card Settings (Board Settings / Card, and the opened card's + Add): every
// pane's field table has Select all / Select none under each column's
// heading; headings and the cells under them start at the same x; the popup is
// wide enough that no row's label is cut. docs/Features/Cards/Card-Fields.md
// Run: node tests/cardSettingsSelectAllAligned.test.cjs
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const f = require('../models/lib/cardShownFields.js');
const root = path.join(__dirname, '..');
const read = p => fs.readFileSync(path.join(root, p), 'utf8');
let passed = 0;
function test(name, fn) { fn(); passed += 1; console.log(' ok -', name); }
console.log('cardSettingsSelectAllAligned:');
const jade = read('client/components/sidebar/sidebar.jade');
const popup = jade.slice(jade.indexOf('template(name="boardCardSettingsPopup")')).split('\ntemplate(')[0];
const js = read('client/components/sidebar/sidebar.js');
const css = read('client/components/sidebar/sidebar.css');
const handler = js.slice(js.indexOf("async 'click .js-card-settings-select'"), js.indexOf("'keydown .js-card-settings-select'"));
test('Select all and Select none under every column heading, for who may change it', () => {
const head = popup.slice(popup.indexOf('.card-settings-pane-head'), popup.indexOf('each row in sectionSettingsRows'));
for (const column of ['minicard', 'card', 'always']) {
assert.match(head, new RegExp(`if canModifyBoard\\n\\s+\\+cardSettingsSelectButtons\\(column="${column}"\\)`), column);
}
// On this card is drawn only for who may change the card (settingsCardId).
assert.match(head, /if settingsCardId\n\s+span\.card-settings-side-head\.card-settings-side-head-this\n\s+span \{\{_ 'card-settings-this-card'\}\}\n\s+\+cardSettingsSelectButtons\(column="this"\)/);
assert.strictEqual((head.match(/\+cardSettingsSelectButtons/g) || []).length, 4, 'one pair per column');
const buttons = jade.slice(jade.indexOf('template(name="cardSettingsSelectButtons")')).split('\ntemplate(')[0];
assert.match(buttons, /js-card-settings-select\(data-column="\{\{column\}\}" data-value="all" role="button" tabindex="0"\) \{\{_ 'select-all'\}\}/);
assert.match(buttons, /js-card-settings-select\(data-column="\{\{column\}\}" data-value="none" role="button" tabindex="0"\) \{\{_ 'select-none'\}\}/);
// Both labels exist in every language.
const dir = path.join(root, 'imports/i18n/data');
for (const file of fs.readdirSync(dir)) {
const data = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
assert.ok(data['select-all'] && data['select-none'], file);
}
});
test('the handler: each column as its own checkboxes would set it, gated the same way', () => {
assert.ok(handler.length > 200, 'found');
// On this card: one write of the card, through the same rule, with the board.
assert.match(handler, /if \(!card \|\| !Utils\.canModifyCard\(card\)\) return;/);
assert.match(handler, /next = cardShownFields\.cardFieldsAfter\(\{ \.\.\.card, \.\.\.next \}, key, on, counts, board\);/);
assert.match(handler, /await Cards\.updateAsync\(card\._id, \{ \$set: next \}\);/);
// The board's columns: a board admin only.
assert.ok(handler.indexOf('if (!(Utils.canModifyBoard && Utils.canModifyBoard())) return;') < handler.indexOf("column === 'always'"));
assert.match(handler, /await board\.setCardFieldAlwaysShown\(keysOf\('\.js-card-always-show'\), on\);/);
// Negative: a personal row, a disabled cell and one already as asked are left alone.
assert.match(handler, /\.filter\(cell => !cell\.classList\.contains\('card-settings-row-personal'\)\)/);
assert.match(handler, /\.filter\(toggle => toggle && !toggle\.classList\.contains\('is-disabled'\) && isOn\(toggle\) !== on\);/);
// Scrum rows are one write, or a revision conflict fails all but the first.
assert.match(handler, /await Meteor\.callAsync\('scrum\.configure', board\._id, \{ visibility: scrum \}, board\.scrumRevision \|\| 0\);/);
assert.match(handler, /cells\.filter\(t => !t\.classList\.contains\('js-scrum-field-toggle'\)\)\.forEach\(toggle => toggle\.click\(\)\);/);
assert.ok(!/js-scrum-field-toggle'\)\)\.forEach\(toggle => toggle\.click/.test(handler.replace("!t.classList.contains('js-scrum-field-toggle'))", '')), 'negative: no Scrum row clicked one by one');
});
test('Show empty: Select all and Select none set the whole list at once', () => {
assert.deepStrictEqual(f.alwaysShownAfter({}, ['labels', 'dueDate'], true), ['dueDate', 'labels', 'descriptionTitle', 'descriptionText', 'attachments', 'comments']);
assert.deepStrictEqual(f.alwaysShownAfter({}, ['descriptionTitle', 'descriptionText', 'attachments', 'comments'], false), []);
assert.deepStrictEqual(f.alwaysShownAfter({ cardFieldsAlwaysShown: ['labels'] }, ['nope', '$where'], true), ['labels'], 'negative: unknown keys are not added');
assert.deepStrictEqual(f.alwaysShownAfter({ cardFieldsAlwaysShown: ['labels'] }, 'dueDate', true), ['dueDate', 'labels'], 'one key still works');
});
test('a heading and its cells start at the same x', () => {
// The bug: the heading line had the smaller font, and its em columns were
// narrower than the rows' - each heading drifted left of its column.
const headRule = css.slice(css.indexOf('.card-settings-pane-head {'), css.indexOf('}', css.indexOf('.card-settings-pane-head {')));
assert.ok(!/font-size/.test(headRule), 'negative: no font-size on the heading line itself');
assert.match(css, /\.card-settings-pane-head > span \{\n\s+font-size: calc\(12px \* var\(--wekan-ui-font-scale, 1\)\);/);
// Every item starts at its column's start edge.
assert.match(css, /grid-template-columns: 9em 9em 8em minmax\(10em, 1fr\);\n\s+column-gap: 14px;\n\s+align-items: center;\n[\s\S]{0,200}justify-items: start;\n\s+text-align: start;/);
assert.match(css, /\.card-settings-side \{\n\s+display: inline-flex;\n\s+align-items: center;\n\s+justify-content: flex-start;\n\s+justify-self: start;/);
assert.match(css, /\.card-settings-side > a\.flex \{\n\s+margin: 0;/);
// Negative: nothing centres or right-aligns a cell of the table.
// (The position number keeps text-align: end inside its own min-width, so
// 9 and 19 line up - that is within a cell, not the cell's place.)
const table = css.slice(css.indexOf('.card-settings-pane-head,\n.card-settings-line {'), css.indexOf('/* One side of the same popup'))
.replace(/\.card-settings-position \{[^}]*\}/, '');
assert.ok(!/(justify-content|justify-self|justify-items|text-align):\s*(center|end|right|flex-end)/.test(table), 'negative: no centred or right-aligned cell');
});
test('wide enough for every label where the window is', () => {
assert.match(read('client/lib/popupOffset.js'), /const CARD_SETTINGS_MAX_WIDTH = 1200;/);
assert.match(read('client/components/main/popup.css'), /\.pop-over\[data-popup='boardCardSettingsPopup'\] \{[^}]*width: min\(96vw, 1200px\) !important;/);
// A label longer than its column wraps in it instead of being cut.
assert.match(css, /\.card-settings-line-label \{[^}]*min-width: 0;[^}]*overflow-wrap: anywhere;/);
});
console.log(`cardSettingsSelectAllAligned: ${passed} passed`);
+6 -4
View File
@@ -152,14 +152,16 @@ test('Checklists: the bottom controls share a line, and the bottom "+" waits for
const offsetSrc = read('client/lib/popupOffset.js');
const offset = vm.runInNewContext(`(() => { ${offsetSrc.replace(/^export \{[^}]*\};?\s*$/m, '')}\n return { computePopupOffset, CARD_SETTINGS_MAX_WIDTH }; })()`);
test('the popup is a centred panel as wide (up to 900px) and tall as the window', () => {
test('the popup is a centred panel as wide (up to 1200px) and tall as the window', () => {
// 1800px fitted two long lists side by side; the Admin Panel's menu and pane
// (docs/Features/Cards/Card-Fields.md) need a row's label and its two sides.
const base = { popupName: 'boardCardSettingsPopup', viewportHeight: 1080, openerTopVp: 40, openerBottomVp: 60,
openerLeftVp: 1700, scrollTop: 0, scrollLeft: 0 };
const wide = offset.computePopupOffset({ ...base, viewportWidth: 1920 });
assert.strictEqual(offset.CARD_SETTINGS_MAX_WIDTH, 900);
assert.strictEqual(wide.left, 510);
// 1200px since the Show empty column: the menu, four checkbox columns and
// the whole label of a row, nothing clipped.
assert.strictEqual(offset.CARD_SETTINGS_MAX_WIDTH, 1200);
assert.strictEqual(wide.left, 360);
assert.strictEqual(wide.top, 12);
assert.strictEqual(wide.maxHeight, 1056);
const narrow = offset.computePopupOffset({ ...base, viewportWidth: 900, viewportHeight: 768 });
@@ -167,7 +169,7 @@ test('the popup is a centred panel as wide (up to 900px) and tall as the window'
// A phone is still the full-screen sheet.
assert.deepStrictEqual({ ...offset.computePopupOffset({ ...base, viewportWidth: 600 }) }, { left: 0, top: 0 });
// The CSS says the same width.
assert.match(popupCss, /\.pop-over\[data-popup='boardCardSettingsPopup'\] \{[^}]*width: min\(96vw, 900px\) !important;[^}]*max-height: calc\(100dvh - 24px\) !important;/);
assert.match(popupCss, /\.pop-over\[data-popup='boardCardSettingsPopup'\] \{[^}]*width: min\(96vw, 1200px\) !important;[^}]*max-height: calc\(100dvh - 24px\) !important;/);
});
test('each row is one line: its label, then the minicard cell and the card cell', () => {
@@ -212,3 +212,55 @@ test('Card Settings opens on Card, and "Show empty fields" shows every field whi
await popup.locator('.js-card-settings-menu[data-id="labels"]').click();
await expect(popup.locator('.js-card-always-show[data-key="labels"]')).toHaveAttribute('aria-checked', 'true');
});
// Every pane: Select all / Select none under each column heading; a heading
// and the cells under it start at the same x; no label is cut in a 1400px window.
test('Card Settings: Select all / Select none per column, headings aligned with their cells, labels whole', async ({ boardPage: page, board }) => {
await page.setViewportSize({ width: 1400, height: 1000 });
const card = db.findOne('cards', { boardId: board.boardId, title: 'Alpha Card' });
db.updateOne('boards', { _id: board.boardId }, { $set: { allowsReceivedDate: true, allowsStartDate: true, allowsDueDate: true, allowsEndDate: true }, $unset: { showEmptyCardFields: '', cardFieldsAlwaysShown: '' } });
db.updateOne('cards', { _id: card._id }, { $unset: { shownFields: '', hiddenFields: '', receivedAt: '', startAt: '', dueAt: '', endAt: '' } });
const details = await openCard(page, board);
await details.locator('.js-open-card-add-fields').click();
const popup = addPopup(page);
await popup.locator('.js-card-settings-menu[data-id="date"]').click();
// Aligned: each heading starts where its column's cells start.
for (const side of ['this', 'minicard', 'card', 'always']) {
const head = await popup.locator(`.card-settings-side-head-${side}`).boundingBox();
const sel = side === 'this' ? '.card-settings-side-this' : side === 'always' ? '.card-settings-side-always' : `.card-settings-side-${side}`;
const cell = await popup.locator(`.card-settings-line ${sel}`).first().boundingBox();
expect(Math.abs(head.x - cell.x)).toBeLessThan(2);
}
const headLabel = await popup.locator('.card-settings-pane-head-label').boundingBox();
const rowLabel = await popup.locator('.card-settings-line-label').first().boundingBox();
expect(Math.abs(headLabel.x - rowLabel.x)).toBeLessThan(2);
// Whole: every label ends inside the pane.
const pane = await popup.locator('.main-body').boundingBox();
for (const label of await popup.locator('.card-settings-line-label').all()) {
const box = await label.boundingBox();
expect(box.x + box.width).toBeLessThanOrEqual(pane.x + pane.width + 1);
}
// On this card: Select all shows the four empty dates on the card, Select none hides them.
await popup.locator('.card-settings-side-head-this .js-card-settings-select[data-value="all"]').click();
await expect.poll(() => (db.findOne('cards', { _id: card._id }).shownFields || []).length).toBeGreaterThanOrEqual(4);
await expect(popup.locator('.js-card-add-field[aria-checked="false"]')).toHaveCount(0);
await popup.locator('.card-settings-side-head-this .js-card-settings-select[data-value="none"]').click();
await expect(popup.locator('.js-card-add-field[aria-checked="true"]')).toHaveCount(0);
// Show empty: Select all writes the board's list in one go.
await popup.locator('.card-settings-side-head-always .js-card-settings-select[data-value="all"]').click();
await expect.poll(() => db.findOne('boards', { _id: board.boardId }).cardFieldsAlwaysShown).toEqual(expect.arrayContaining(['receivedDate', 'startDate', 'dueDate', 'endDate']));
await popup.locator('.card-settings-side-head-always .js-card-settings-select[data-value="none"]').click();
await expect(popup.locator('.js-card-always-show[aria-checked="true"]')).toHaveCount(0);
// Show on Card: Select none unticks the section's rows, Select all ticks them again.
await popup.locator('.card-settings-side-head-card .js-card-settings-select[data-value="none"]').click();
await expect.poll(() => db.findOne('boards', { _id: board.boardId }).allowsDueDate).toBe(false);
await expect(popup.locator('.js-card-field-order-row[data-side="card"] .card-field-order-toggle[aria-checked="true"]')).toHaveCount(0);
await popup.locator('.card-settings-side-head-card .js-card-settings-select[data-value="all"]').click();
await expect.poll(() => db.findOne('boards', { _id: board.boardId }).allowsDueDate).toBe(true);
// Negative: another section's rows are untouched.
expect(db.findOne('boards', { _id: board.boardId }).allowsMembers).not.toBe(false);
});