Added Date Format setting to Opened Card.

Thanks to xet7 !

Fixes #2011,
fixes #1176
This commit is contained in:
Lauri Ojansivu 2025-10-20 01:36:44 +03:00
parent 516552cce6
commit 2dd3916f7e
8 changed files with 161 additions and 13 deletions

View file

@ -1,8 +1,10 @@
import { TAPi18n } from '/imports/i18n'; import { TAPi18n } from '/imports/i18n';
import { DatePicker } from '/client/lib/datepicker'; import { DatePicker } from '/client/lib/datepicker';
import { ReactiveCache } from '/imports/reactiveCache';
import { import {
formatDateTime, formatDateTime,
formatDate, formatDate,
formatDateByUserPreference,
formatTime, formatTime,
getISOWeek, getISOWeek,
isValidDate, isValidDate,
@ -177,7 +179,9 @@ CardCustomField.register('cardCustomField');
} }
showDate() { showDate() {
return calendar(this.date.get()); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
showISODate() { showISODate() {

View file

@ -3,6 +3,7 @@ import { DatePicker } from '/client/lib/datepicker';
import { import {
formatDateTime, formatDateTime,
formatDate, formatDate,
formatDateByUserPreference,
formatTime, formatTime,
getISOWeek, getISOWeek,
isValidDate, isValidDate,
@ -131,7 +132,9 @@ const CardDate = BlazeComponent.extendComponent({
}, },
showDate() { showDate() {
return calendar(this.date.get()); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
}, },
showISODate() { showISODate() {
@ -166,7 +169,10 @@ class CardReceivedDate extends CardDate {
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-received-on')} ${format(this.date.get(), 'LLLL')}`; const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-received-on')} ${formattedDate}`;
} }
events() { events() {
@ -201,7 +207,10 @@ class CardStartDate extends CardDate {
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-start-on')} ${format(this.date.get(), 'LLLL')}`; const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-start-on')} ${formattedDate}`;
} }
events() { events() {
@ -237,7 +246,10 @@ class CardDueDate extends CardDate {
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-due-on')} ${format(this.date.get(), 'LLLL')}`; const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${TAPi18n.__('card-due-on')} ${formattedDate}`;
} }
events() { events() {
@ -315,7 +327,10 @@ class CardCustomFieldDate extends CardDate {
} }
showTitle() { showTitle() {
return `${format(this.date.get(), 'LLLL')}`; const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
const formattedDate = formatDateByUserPreference(this.date.get(), dateFormat, true);
return `${formattedDate}`;
} }
classes() { classes() {
@ -334,7 +349,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
} }
showDate() { showDate() {
return format(this.date.get(), 'L'); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
}.register('minicardReceivedDate')); }.register('minicardReceivedDate'));
@ -344,7 +361,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
} }
showDate() { showDate() {
return format(this.date.get(), 'YYYY-MM-DD HH:mm'); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
}.register('minicardStartDate')); }.register('minicardStartDate'));
@ -354,7 +373,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
} }
showDate() { showDate() {
return format(this.date.get(), 'YYYY-MM-DD HH:mm'); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
}.register('minicardDueDate')); }.register('minicardDueDate'));
@ -364,7 +385,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
} }
showDate() { showDate() {
return format(this.date.get(), 'YYYY-MM-DD HH:mm'); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
}.register('minicardEndDate')); }.register('minicardEndDate'));
@ -374,7 +397,9 @@ CardCustomFieldDate.register('cardCustomFieldDate');
} }
showDate() { showDate() {
return format(this.date.get(), 'L'); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
}.register('minicardCustomFieldDate')); }.register('minicardCustomFieldDate'));
@ -391,7 +416,9 @@ class VoteEndDate extends CardDate {
return classes; return classes;
} }
showDate() { showDate() {
return format(this.date.get(), 'L') + ' ' + format(this.date.get(), 'HH:mm'); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`; return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
@ -418,7 +445,9 @@ class PokerEndDate extends CardDate {
return classes; return classes;
} }
showDate() { showDate() {
return format(this.date.get(), 'l LT'); const currentUser = ReactiveCache.getCurrentUser();
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
return formatDateByUserPreference(this.date.get(), dateFormat, true);
} }
showTitle() { showTitle() {
return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`; return `${TAPi18n.__('card-end-on')} ${format(this.date.get(), 'LLLL')}`;

View file

@ -1,3 +1,31 @@
/* Date Format Selector */
.card-details-item-date-format {
margin-bottom: 10px;
}
.card-details-item-date-format .card-details-item-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.card-details-item-date-format .js-date-format-selector {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
font-size: 14px;
cursor: pointer;
}
.card-details-item-date-format .js-date-format-selector:focus {
outline: none;
border-color: #007cba;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
}
.assignee { .assignee {
border-radius: 3px; border-radius: 3px;
display: block; display: block;

View file

@ -113,6 +113,16 @@ template(name="cardDetails")
if currentBoard.hasAnyAllowsDate if currentBoard.hasAnyAllowsDate
hr hr
.card-details-item.card-details-item-date-format
h3.card-details-item-title
| 📅
| {{_ '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.allowsReceivedDate if currentBoard.allowsReceivedDate
.card-details-item.card-details-item-received .card-details-item.card-details-item-received
h3.card-details-item-title h3.card-details-item-title

View file

@ -306,6 +306,10 @@ BlazeComponent.extendComponent({
const $tooltip = this.$('.card-details-header .copied-tooltip'); const $tooltip = this.$('.card-details-header .copied-tooltip');
Utils.showCopied(promise, $tooltip); Utils.showCopied(promise, $tooltip);
}, },
'change .js-date-format-selector'(event) {
const dateFormat = event.target.value;
Meteor.call('changeDateFormat', dateFormat);
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'), 'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
'submit .js-card-description'(event) { 'submit .js-card-description'(event) {
event.preventDefault(); event.preventDefault();
@ -568,6 +572,11 @@ Template.cardDetails.helpers({
let ret = !!Utils.getPopupCardId(); let ret = !!Utils.getPopupCardId();
return ret; return ret;
}, },
isDateFormat(format) {
const currentUser = ReactiveCache.getCurrentUser();
if (!currentUser) return format === 'YYYY-MM-DD';
return currentUser.getDateFormat() === format;
},
// Upload progress helpers // Upload progress helpers
hasActiveUploads() { hasActiveUploads() {
return uploadProgressManager.hasActiveUploads(this._id); return uploadProgressManager.hasActiveUploads(this._id);

View file

@ -354,6 +354,10 @@
"custom-field-text": "Text", "custom-field-text": "Text",
"custom-fields": "Custom Fields", "custom-fields": "Custom Fields",
"date": "Date", "date": "Date",
"date-format": "Date Format",
"date-format-yyyy-mm-dd": "YYYY-MM-DD HH:MM",
"date-format-dd-mm-yyyy": "DD-MM-YYYY HH:MM",
"date-format-mm-dd-yyyy": "MM-DD-YYYY HH:MM",
"decline": "Decline", "decline": "Decline",
"default-avatar": "Default avatar", "default-avatar": "Default avatar",
"delete": "Delete", "delete": "Delete",

View file

@ -36,6 +36,44 @@ export function formatDate(date) {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
/**
* Format a date according to user's preferred format
* @param {Date|string} date - Date to format
* @param {string} format - Format string (YYYY-MM-DD, DD-MM-YYYY, MM-DD-YYYY)
* @param {boolean} includeTime - Whether to include time (HH:MM)
* @returns {string} Formatted date string
*/
export function formatDateByUserPreference(date, format = 'YYYY-MM-DD', includeTime = true) {
const d = new Date(date);
if (isNaN(d.getTime())) return '';
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
let dateString;
switch (format) {
case 'DD-MM-YYYY':
dateString = `${day}-${month}-${year}`;
break;
case 'MM-DD-YYYY':
dateString = `${month}-${day}-${year}`;
break;
case 'YYYY-MM-DD':
default:
dateString = `${year}-${month}-${day}`;
break;
}
if (includeTime) {
return `${dateString} ${hours}:${minutes}`;
}
return dateString;
}
/** /**
* Format a time to HH:mm format * Format a time to HH:mm format
* @param {Date|string} date - Date to format * @param {Date|string} date - Date to format

View file

@ -465,6 +465,15 @@ Users.attachSchema(
type: Boolean, type: Boolean,
defaultValue: true, defaultValue: true,
}, },
'profile.dateFormat': {
/**
* User-specified date format for displaying dates (includes time HH:MM).
*/
type: String,
optional: true,
allowedValues: ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM-DD-YYYY'],
defaultValue: 'YYYY-MM-DD',
},
'profile.zoomLevel': { 'profile.zoomLevel': {
/** /**
* User-specified zoom level for board view (1.0 = 100%, 1.5 = 150%, etc.) * User-specified zoom level for board view (1.0 = 100%, 1.5 = 150%, etc.)
@ -1049,6 +1058,11 @@ Users.helpers({
return profile.startDayOfWeek; return profile.startDayOfWeek;
}, },
getDateFormat() {
const profile = this.profile || {};
return profile.dateFormat || 'YYYY-MM-DD';
},
getTemplatesBoardId() { getTemplatesBoardId() {
return (this.profile || {}).templatesBoardId; return (this.profile || {}).templatesBoardId;
}, },
@ -1452,6 +1466,14 @@ Users.mutations({
}; };
}, },
setDateFormat(dateFormat) {
return {
$set: {
'profile.dateFormat': dateFormat,
},
};
},
setBoardView(view) { setBoardView(view) {
return { return {
$set: { $set: {
@ -1597,6 +1619,10 @@ Meteor.methods({
check(startDay, Number); check(startDay, Number);
ReactiveCache.getCurrentUser().setStartDayOfWeek(startDay); ReactiveCache.getCurrentUser().setStartDayOfWeek(startDay);
}, },
changeDateFormat(dateFormat) {
check(dateFormat, String);
ReactiveCache.getCurrentUser().setDateFormat(dateFormat);
},
applyListWidth(boardId, listId, width, constraint) { applyListWidth(boardId, listId, width, constraint) {
check(boardId, String); check(boardId, String);
check(listId, String); check(listId, String);