Separate card and minicard custom field visibility.

Allow checkbox values to toggle both ways from the reactive field context. Add board settings that show custom fields on opened cards by default and hide them on minicards by default, with independent menu controls for each surface.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu committed 2026-08-18 19:23:22 +03:00
1 parent 269351e852
commit 1d4ffd4080
11 files changed
+112 -13

No files matched your search

+6 -6
View File
@@ -134,12 +134,12 @@ Template['cardCustomField-checkbox'].events({
async 'click .js-card-custom-field-checkbox .check-box-container'(event, tpl) {
event.preventDefault();
event.stopPropagation();
const sourceCard = tpl.card?.getRealCard?.() || tpl.card;
const storedField = (sourceCard?.customFields || []).find(
field => field?._id === tpl.customFieldId,
);
if (!sourceCard || !storedField) return;
const value = !Boolean(storedField.value);
// This template context is rebuilt from customFieldsWD whenever the saved
// value changes. Using the Card object captured at onCreated kept the old
// value after the first click, so a second click tried to save true again.
const currentField = Template.currentData();
if (!currentField || currentField._id !== tpl.customFieldId) return;
const value = !Boolean(currentField.value);
try {
await Meteor.callAsync(
'setCardCustomFieldCheckbox', tpl.card.getRealId(), tpl.customFieldId, value);
+1 -1
View File
@@ -446,7 +446,7 @@ template(name="cardDetails")
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 canModifyCard
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,
+4
View File
@@ -550,6 +550,10 @@ Template.cardDetails.onDestroyed(function () {
});
Template.cardDetails.helpers({
canShowCustomFieldsOnCard() {
const board = this?.board?.();
return Utils.canModifyCard(this) && board?.allowsCustomFields !== false;
},
stickers() {
const card = Template.currentData();
return card && typeof card.getStickers === 'function' ? card.getStickers() : [];
+4 -4
View File
@@ -132,10 +132,10 @@ template(name="minicard")
if hiddenMinicardLabelText
.minicard-label(class="{{labelColorClass color}}" style="{{labelColorStyle color}}" title="{{name}}")
.minicard-custom-fields
each customFieldsWD
if definition.showOnCard
if trueValue
if showCustomFieldsOnMinicard
.minicard-custom-fields
each customFieldsWD
if definition.showOnCard
.minicard-custom-field
// If there is custom field label, show label at left,
// and value at right
+4
View File
@@ -34,6 +34,10 @@ function getMinicardFlag(board, onMinicardField, legacyField, defaultValue) {
// });
Template.minicard.helpers({
showCustomFieldsOnMinicard() {
const board = this.board();
return board?.allowsCustomFieldsOnMinicard === true;
},
// True exactly when the drag handle is rendered (see minicard.jade): the user
// may move the card AND drag handles are on. On a coarse pointer the handle is
// a 48px strip down the leading edge, so the card's own content needs matching
+11
View File
@@ -477,6 +477,17 @@ template(name="boardCardSettingsPopup")
i.fa.fa-file-text-o
| {{_ 'labels'}}
| {{_ 'custom-field-text'}}
.card-settings-row
.card-settings-column
a.flex.js-field-has-custom-fields(title="{{_ 'custom-fields'}}" class="{{#if allowsCustomFields}}is-checked{{/if}}")
i.fa(class="{{#if allowsCustomFields}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
a.flex.js-field-has-custom-fields-on-minicard(title="{{_ 'custom-fields'}}" class="{{#if allowsCustomFieldsOnMinicard}}is-checked{{/if}}")
i.fa(class="{{#if allowsCustomFieldsOnMinicard}}fa-check{{else}}fa-square-o{{/if}}")
.card-settings-column
span
i.fa.fa-list-alt
| {{_ 'custom-fields'}}
.card-settings-row
.card-settings-column
a.flex.js-field-has-card-number(title="{{_ 'card'}} {{_ 'number'}}" class="{{#if allowsCardNumber}}is-checked{{/if}}")
+22
View File
@@ -1700,6 +1700,14 @@ Template.boardCardSettingsPopup.helpers({
const currentBoard = ReactiveCache.getBoard(boardId);
return getMinicardSetting(currentBoard, 'allowsLabelsOnMinicard', 'allowsLabels', true);
},
allowsCustomFields() {
const board = ReactiveCache.getBoard(Session.get('currentBoard'));
return board ? board.allowsCustomFields !== false : true;
},
allowsCustomFieldsOnMinicard() {
const board = ReactiveCache.getBoard(Session.get('currentBoard'));
return board?.allowsCustomFieldsOnMinicard === true;
},
allowsShowListsOnMinicard() {
const boardId = Session.get('currentBoard');
const currentBoard = ReactiveCache.getBoard(boardId);
@@ -2001,6 +2009,20 @@ Template.boardCardSettingsPopup.events({
const newValue = !tpl.currentBoard.allowsLabelsOnMinicard;
Boards.update(tpl.currentBoard._id, { $set: { allowsLabelsOnMinicard: newValue } });
},
'click .js-field-has-custom-fields'(evt, tpl) {
evt.preventDefault();
const currentValue = tpl.currentBoard.allowsCustomFields !== false;
Boards.update(tpl.currentBoard._id, {
$set: { allowsCustomFields: !currentValue },
});
},
'click .js-field-has-custom-fields-on-minicard'(evt, tpl) {
evt.preventDefault();
const currentValue = tpl.currentBoard.allowsCustomFieldsOnMinicard === true;
Boards.update(tpl.currentBoard._id, {
$set: { allowsCustomFieldsOnMinicard: !currentValue },
});
},
'click .js-field-has-card-show-lists-on-minicard'(evt, tpl) {
evt.preventDefault();
const newValue = !tpl.currentBoard.allowsShowListsOnMinicard;
+13
View File
@@ -604,6 +604,19 @@ Boards.attachSchema(
defaultValue: true,
},
allowsCustomFields: {
/** Show assigned custom fields on opened cards? */
type: Boolean,
optional: true,
defaultValue: true,
},
allowsCustomFieldsOnMinicard: {
/** Show assigned custom fields on minicards? Opt-in by default. */
type: Boolean,
optional: true,
defaultValue: false,
},
allowsChecklistCountBadgeOnMinicard: {
/**
* Does the board show the compact checklist item-count badge
+2 -2
View File
@@ -28,8 +28,8 @@ test('opened-card checkbox saves its value independently of visibility', () => {
const at = client.indexOf("async 'click .js-card-custom-field-checkbox .check-box-container'");
const body = client.slice(at, client.indexOf('\n },', at));
assert.match(body, /await Meteor\.callAsync\(\s*'setCardCustomFieldCheckbox'/);
assert.match(body, /sourceCard\?\.customFields/);
assert.match(body, /!Boolean\(storedField\.value\)/);
assert.match(body, /Template\.currentData\(\)/);
assert.match(body, /!Boolean\(currentField\.value\)/);
assert.match(body, /event\.stopPropagation\(\)/);
assert.doesNotMatch(body, /tpl\.card\.setCustomField/);
});
@@ -0,0 +1,42 @@
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..');
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
const boards = read('models/boards.js');
const sidebar = read('client/components/sidebar/sidebar.js');
const sidebarTemplate = read('client/components/sidebar/sidebar.jade');
const details = read('client/components/cards/cardDetails.js');
const detailsTemplate = read('client/components/cards/cardDetails.jade');
const minicard = read('client/components/cards/minicard.js');
const minicardTemplate = read('client/components/cards/minicard.jade');
test('custom fields default on for cards and off for minicards', () => {
const cardSetting = boards.slice(boards.indexOf('allowsCustomFields: {'));
assert.match(cardSetting.slice(0, cardSetting.indexOf('},') + 2),
/defaultValue: true/);
const miniSetting = boards.slice(boards.indexOf('allowsCustomFieldsOnMinicard: {'));
assert.match(miniSetting.slice(0, miniSetting.indexOf('},') + 2),
/defaultValue: false/);
assert.match(details, /board\?\.allowsCustomFields !== false/);
assert.match(minicard, /board\?\.allowsCustomFieldsOnMinicard === true/);
});
test('card and minicard menus expose independent Custom Fields rows', () => {
assert.match(sidebarTemplate, /js-field-has-custom-fields/);
assert.match(sidebarTemplate, /js-field-has-custom-fields-on-minicard/);
assert.match(sidebarTemplate, /\{\{_ 'custom-fields'\}\}/);
assert.match(sidebar, /'click \.js-field-has-custom-fields'/);
assert.match(sidebar, /\$set: \{ allowsCustomFields: !currentValue \}/);
assert.match(sidebar, /'click \.js-field-has-custom-fields-on-minicard'/);
assert.match(sidebar,
/\$set: \{ allowsCustomFieldsOnMinicard: !currentValue \}/);
});
test('each card surface is gated only by its matching setting', () => {
assert.match(detailsTemplate, /if canShowCustomFieldsOnCard[\s\S]*each customFieldsWD/);
assert.match(minicardTemplate,
/if showCustomFieldsOnMinicard[\s\S]*each customFieldsWD/);
});
@@ -309,6 +309,9 @@ test.describe('Cards – operations', () => {
db.updateOne('boards', { _id: source.boardId }, {
$set: { labels: [{ _id: labelId, name: 'Source Label', color: 'green' }] },
});
db.updateOne('boards', { _id: board.boardId }, {
$set: { allowsCustomFieldsOnMinicard: true },
});
db.insertOne('customFields', {
_id: customFieldId,
boardIds: [source.boardId],