Files
Lauri Ojansivu 6d5f24ee27 Large board: read only the user fields each card and list needs.
Opening a card writes the user document (profile.cardLastViews, #3078). The
board view (Utils.storedBoardView and allowBoardView, which every list's card
loop reads) and the helpers every minicard runs read the WHOLE user document,
so that one write re-ran every list's card loop, and Blaze then re-evaluated
every minicard: 37,452 Tracker invalidations for one card open on a 40-card
board. Measured in the running app, it is now 390.

client/lib/currentUserWith.js reads the current user with only the named
fields; minimongo re-runs a field-limited query only when a projected field
changes. The board view, feature preview, unread comments, folds, drag
handles, label text, dependency layers, date format, week number, mobile mode
and the admin-only custom field check now use it. setCardLastViewed $sets one
entry instead of rewriting the whole map, which is also capped at 5,000.

Thanks to markusst1982 and xet7 !

Fixes #6745.
2026-10-07 03:08:48 +03:00

140 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Utils } from '/client/lib/utils';
const { resolveDateFormat } = require('/models/lib/dateFormatPolicy');
import { ReactiveCache } from '/imports/reactiveCache';
import { currentUserWith } from '/client/lib/currentUserWith';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
import { formatJalaliDate, gregorianToJalali } from '/imports/lib/jalaliDate';
import { TAPi18n } from '/imports/i18n';
const { CALENDAR_SYSTEMS, formatNativeCalendarDate, nativeCalendarParts } = require('/imports/lib/calendarSystems');
const { calendarMonthRange, shiftedDay } = require('/imports/lib/calendarMonth');
// #6745: every date badge on every minicard formats through here, so read only
// the date fields of the user - a card open (a user write) must not re-run them.
const DATE_USER_FIELDS = ['profile.calendarSystem', 'profile.dateFormat', 'profile.dateFormatOverride'];
export function hasDateFormatPreference() {
return !!(currentUserWith(DATE_USER_FIELDS)?.profile?.dateFormatOverride ||
Utils.getCurrentBoard()?.dateFormatOverride || ReactiveCache.getCurrentSetting()?.hideDateFormat);
}
export function dateDisplayPreferences() {
const setting = ReactiveCache.getCurrentSetting();
const user = currentUserWith(DATE_USER_FIELDS);
const board = Utils.getCurrentBoard();
if (user) {
return {
calendarSystem: user.getCalendarSystem ? user.getCalendarSystem() : 'gregorian',
dateFormat: resolveDateFormat(user.getDateFormat ? user.getDateFormat() : null, setting, board, user.profile?.dateFormatOverride),
};
}
try {
return {
calendarSystem: window.localStorage.getItem('calendarSystem') || 'gregorian',
dateFormat: resolveDateFormat(null, setting, board),
};
} catch (error) {
return { calendarSystem: 'gregorian', dateFormat: resolveDateFormat(null, setting, board) };
}
}
// Only rendered text belongs here. ISO attributes, picker values and persisted
// dates must continue to use Gregorian formatting. A supplied callback keeps
// each existing Gregorian display's locale, precision and layout.
export function formatDateForDisplay(date, includeTime = true, gregorianFormatter) {
if (date === null || date === undefined || date === '') return '';
const value = new Date(date);
if (Number.isNaN(value.getTime())) return '';
const { calendarSystem, dateFormat } = dateDisplayPreferences();
if (calendarSystem === 'jalali') {
try {
return formatJalaliDate(value, dateFormat, includeTime);
} catch (error) {
// Intl's Persian calendar covers dates beyond the tabular converter's
// finite range without displaying a different calendar.
return formatNativeCalendarDate(value, 'persian', dateFormat, includeTime, TAPi18n.getLanguage() || 'en') || '';
}
} else if (calendarSystem !== 'gregorian') {
const system = CALENDAR_SYSTEMS.find(item => item.value === calendarSystem);
const text = system && formatNativeCalendarDate(
value, system.intl, dateFormat, includeTime, TAPi18n.getLanguage() || 'en',
);
if (system) return text || '';
}
return gregorianFormatter && !hasDateFormatPreference() && !dateFormat.endsWith('-date-only')
? gregorianFormatter(value)
: formatDateByUserPreference(value, dateFormat, includeTime);
}
// FullCalendar 5.11.5 exposes its displayed wall-calendar fields to formatting
// callbacks. Construct from those fields instead of shifting its UTC marker
// through the browser's time zone. Grid placement and navigation stay native.
export function calendarDateDisplayOptions(calendarId) {
const { calendarSystem } = dateDisplayPreferences();
if (calendarSystem === 'gregorian') return {};
const system = CALENDAR_SYSTEMS.find(item => item.value === calendarSystem);
if (!system) return {};
const displayMarker = marker => formatDateForDisplay(
new Date(marker.year, marker.month, marker.day, 12), false,
);
const navigate = direction => {
const calendar = document.getElementById(calendarId)?._wekanCalendar;
if (!calendar) return;
if (!['selectedCalendarMonth', 'selectedCalendarListMonth'].includes(calendar.view.type)) {
calendar[direction < 0 ? 'prev' : 'next']();
return;
}
const range = calendarMonthRange(calendar.getDate(), system.intl);
calendar.gotoDate(direction < 0 ? shiftedDay(range.start, -1) : range.end);
};
return {
// Built-in month views have a fixed Gregorian duration that takes
// precedence over visibleRange. Inherit the duration-free base views.
initialView: 'selectedCalendarMonth',
views: {
selectedCalendarMonth: {
type: 'dayGrid', monthMode: true, fixedWeekCount: false,
visibleRange: date => calendarMonthRange(date, system.intl),
},
selectedCalendarListMonth: {
type: 'list', visibleRange: date => calendarMonthRange(date, system.intl),
},
},
customButtons: {
dayGridMonth: { text: TAPi18n.__('month'), click: () => {
document.getElementById(calendarId)?._wekanCalendar.changeView('selectedCalendarMonth');
} },
listMonth: { text: TAPi18n.__('list'), click: () => {
document.getElementById(calendarId)?._wekanCalendar.changeView('selectedCalendarListMonth');
} },
prev: { text: TAPi18n.__('previous'), click: () => navigate(-1) },
next: { text: TAPi18n.__('next'), click: () => navigate(1) },
},
titleFormat(info) {
const start = displayMarker(info.start);
const end = info.end && displayMarker(info.end);
return end && end !== start ? `${start}${info.defaultSeparator || ' – '}${end}` : start;
},
dayCellContent(info) {
try {
if (calendarSystem !== 'jalali') {
const parts = nativeCalendarParts(info.date, system.intl, TAPi18n.getLanguage() || 'en');
return parts ? parts.day : info.dayNumberText;
}
return String(gregorianToJalali(
info.date.getFullYear(), info.date.getMonth() + 1, info.date.getDate(),
).jd);
} catch (error) {
return info.dayNumberText;
}
},
dayHeaderContent(info) {
// Month-view column headings contain weekdays, with no date to convert.
return ['dayGridMonth', 'selectedCalendarMonth'].includes(info.view.type)
? info.text : formatDateForDisplay(info.date, false);
},
listDayFormat(info) { return displayMarker(info.date); },
// The list header has two date slots. Use one selected-calendar date.
listDaySideFormat() { return ''; },
};
}