Merge pull request #6162 from harryadel/chore/fullcalendar-npm-moment-migration

fullcalendar npm moment migration
This commit is contained in:
Lauri Ojansivu 2026-02-25 12:34:44 +02:00 committed by GitHub
commit 3e400820e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 554 additions and 300 deletions

View file

@ -97,7 +97,7 @@
"Avatar": true,
"Avatars": true,
"BlazeComponent": false,
"CollectionHooks": false,
"ESSearchResults": false,
"FastRender": false,
@ -105,7 +105,6 @@
"FS": false,
"getSlug": false,
"Migrations": false,
"moment": false,
"Mousetrap": false,
"Picker": false,
"Presence": true,

View file

@ -78,7 +78,6 @@ peerlibrary:blaze-components
ejson@1.1.3
logging@1.3.3
wekan-fullcalendar
momentjs:moment@2.29.3
wekan-fontawesome
useraccounts:flow-routing-extra

View file

@ -71,7 +71,6 @@ minimongo@1.9.4
modern-browsers@0.1.10
modules@0.20.0
modules-runtime@0.13.1
momentjs:moment@2.29.3
mongo@1.16.10
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
@ -142,7 +141,7 @@ wekan-accounts-lockout@1.1.0
wekan-accounts-oidc@1.0.10
wekan-accounts-sandstorm@0.9.0
wekan-fontawesome@6.4.2
wekan-fullcalendar@3.10.5
wekan-fullcalendar@5.11.5
wekan-ldap@0.1.0
wekan-markdown@1.0.9
wekan-oidc@1.1.0

View file

@ -185,3 +185,19 @@ Key files involved in the async migration:
| `server/publications/*.js` | Meteor publications |
| `server/rulesHelper.js` | Rule trigger/action evaluation |
| `server/cronMigrationManager.js` | Cron-based migration jobs |
---
## 10. FullCalendar Versioning Note (Post-3.0 Follow-Up)
`wekan-fullcalendar` is currently migrated from legacy Meteor package globals to npm-based **FullCalendar 5.11.5** to keep Meteor 2.16 and 3.0 dual compatibility stable.
**Why pinned for now:**
- Avoids introducing additional breaking changes during core Meteor async migration.
- Keeps compatibility with current Blaze/jQuery-era integration points while removing `momentjs:moment` Meteor package dependency.
**After Meteor 3.0 lands (recommended follow-up):**
1. Re-evaluate upgrading FullCalendar to latest stable major.
2. Re-test plugin API differences (especially view names, callback signatures, locale/time formatting, CSS entry points).
3. Verify Node/runtime compatibility and bundle behavior under Meteor 3's final toolchain.
4. Keep migration isolated in a dedicated PR (separate from async data-layer work) to reduce rollback risk.

View file

@ -189,15 +189,15 @@ template(name="activity")
if(currentData.timeKey)
| {{_ activity.activityType }}
= ' '
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
i(title=currentData.timeValue).activity-meta {{ displayDate currentData.timeValue 'LLL' }}
if (currentData.timeOldValue)
= ' '
| {{{_ "previous_as" }}}
= ' '
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
i(title=currentData.timeOldValue).activity-meta {{ displayDate currentData.timeOldValue 'LLL' }}
= ' @'
else if(currentData.timeValue)
| {{_ activity.activityType currentData.timeValue}}
if($neq mode 'none')
div(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
div(title=activity.createdAt).activity-meta {{ displayDate activity.createdAt }}

View file

@ -32,7 +32,7 @@ template(name="comment")
+viewer
= text
+commentReactions(reactions=reactions commentId=_id)
span(title=createdAt).comment-meta {{ moment createdAt }}
span(title=createdAt).comment-meta {{ displayDate createdAt }}
if($eq currentUser._id userId)
+editOrDeleteComment
else if currentUser.isBoardAdmin

View file

@ -15,7 +15,7 @@ template(name="archivedBoards")
i.fa.fa-undo
| {{_ 'restore-board'}}
= title
span {{ moment archivedAt 'LLL' }}
span {{ displayDate archivedAt 'LLL' }}
else
li.no-items-message {{_ 'no-archived-boards'}}

View file

@ -226,16 +226,22 @@ BlazeComponent.extendComponent({
}
// Observe for new popups/menus and set focus (but exclude swimlane content)
const popupObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1 &&
(node.classList.contains('popup') || node.classList.contains('modal') || node.classList.contains('menu')) &&
!node.closest('.js-swimlanes') &&
!node.closest('.swimlane') &&
!node.closest('.list') &&
!node.closest('.minicard')) {
setTimeout(function() { focusFirstInteractive(node); }, 10);
const popupObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
mutation.addedNodes.forEach(function (node) {
if (
node.nodeType === 1 &&
(node.classList.contains('popup') ||
node.classList.contains('modal') ||
node.classList.contains('menu')) &&
!node.closest('.js-swimlanes') &&
!node.closest('.swimlane') &&
!node.closest('.list') &&
!node.closest('.minicard')
) {
setTimeout(function () {
focusFirstInteractive(node);
}, 10);
}
});
});
@ -898,39 +904,51 @@ BlazeComponent.extendComponent({
document.documentElement.lang = TAPi18n.getLanguage();
this.autorun(function () {
$('#calendar-view').fullCalendar('refetchEvents');
const calendarEl = document.getElementById('calendar-view');
if (calendarEl && calendarEl._wekanCalendar) {
calendarEl._wekanCalendar.refetchEvents();
}
});
},
calendarOptions() {
const t = (key, fallback) => {
const translated = TAPi18n.__(key);
return translated && translated !== key ? translated : fallback;
};
return {
id: 'calendar-view',
defaultView: 'month',
initialView: 'dayGridMonth',
editable: true,
selectable: true,
timezone: 'local',
weekNumbers: true,
// Use non-localized AM/PM time format to avoid confusing notations like 上/下/中
// Use full 'am'/'pm' instead of single-letter 'a'/'p' for clarity
timeFormat: 'h:mma',
slotLabelFormat: 'h:mma',
extraSmallTimeFormat: 'h(:mm)a',
smallTimeFormat: 'h(:mm)a',
mediumTimeFormat: 'h:mma',
hourFormat: 'ha',
noMeridiemTimeFormat: 'h:mm',
header: {
left: 'title today prev,next',
eventTimeFormat: {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short',
},
slotLabelFormat: {
hour: 'numeric',
minute: '2-digit',
meridiem: 'short',
},
headerToolbar: {
left: 'title today prev,next',
center:
'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,listMonth',
'timeGridDay,listDay timeGridWeek,listWeek dayGridMonth,listMonth',
right: '',
},
buttonIcons: false,
buttonText: {
prev: TAPi18n.__('calendar-previous-month-label'), // e.g. "Previous month"
next: TAPi18n.__('calendar-next-month-label'), // e.g. "Next month"
},
ariaLabel: {
prev: TAPi18n.__('calendar-previous-month-label'),
next: TAPi18n.__('calendar-next-month-label'),
prev: t('previous', 'Previous'),
next: t('next', 'Next'),
today: t('today', 'Today'),
day: t('day', 'Day'),
week: t('week', 'Week'),
month: t('month', 'Month'),
list: t('list', 'List'),
},
// height: 'parent', nope, doesn't work as the parent might be small
height: 'auto',
@ -939,12 +957,12 @@ BlazeComponent.extendComponent({
nowIndicator: true,
businessHours: {
// days of week. an array of zero-based day of week integers (0=Sunday)
dow: [1, 2, 3, 4, 5], // Monday - Friday
daysOfWeek: [1, 2, 3, 4, 5], // Monday - Friday
start: '8:00',
end: '18:00',
},
locale: TAPi18n.getLanguage(),
events(start, end, timezone, callback) {
events(fetchInfo, callback) {
const currentBoard = Utils.getCurrentBoard();
const events = [];
const pushEvent = function (card, title, start, end, extraCls) {
@ -970,12 +988,12 @@ BlazeComponent.extendComponent({
});
};
currentBoard
.cardsInInterval(start.toDate(), end.toDate())
.cardsInInterval(fetchInfo.start, fetchInfo.end)
.forEach(function (card) {
pushEvent(card);
});
currentBoard
.cardsDueInBetween(start.toDate(), end.toDate())
.cardsDueInBetween(fetchInfo.start, fetchInfo.end)
.forEach(function (card) {
pushEvent(
card,
@ -989,36 +1007,36 @@ BlazeComponent.extendComponent({
});
callback(events);
},
eventResize(event, delta, revertFunc) {
eventResize(info) {
let isOk = false;
const card = ReactiveCache.getCard(event.id);
const card = ReactiveCache.getCard(info.event.id);
if (card) {
card.setEnd(event.end.toDate());
card.setEnd(info.event.end);
isOk = true;
}
if (!isOk) {
revertFunc();
info.revert();
}
},
eventDrop(event, delta, revertFunc) {
eventDrop(info) {
let isOk = false;
const card = ReactiveCache.getCard(event.id);
const card = ReactiveCache.getCard(info.event.id);
if (card) {
// TODO: add a flag for allDay events
if (!event.allDay) {
if (!info.event.allDay) {
// https://github.com/wekan/wekan/issues/2917#issuecomment-1236753962
//card.setStart(event.start.toDate());
//card.setEnd(event.end.toDate());
card.setDue(event.start.toDate());
//card.setStart(info.event.start);
//card.setEnd(info.event.end);
card.setDue(info.event.start);
isOk = true;
}
}
if (!isOk) {
revertFunc();
info.revert();
}
},
select: function (startDate) {
select: function (selectionInfo) {
const currentBoard = Utils.getCurrentBoard();
const currentUser = ReactiveCache.getCurrentUser();
const modalElement = document.createElement('div');
@ -1030,7 +1048,7 @@ BlazeComponent.extendComponent({
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${TAPi18n.__('r-create-card')}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button type="button" class="close calendar-create-close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@ -1038,7 +1056,7 @@ BlazeComponent.extendComponent({
<input type="text" class="form-control" id="card-title-input" placeholder="">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="create-card-button">${TAPi18n.__('add-card')}</button>
<button type="button" class="primary confirm" id="create-card-button">${TAPi18n.__('add-card')}</button>
</div>
</div>
</div>
@ -1056,7 +1074,7 @@ BlazeComponent.extendComponent({
currentBoard._id,
firstList._id,
myTitle,
startDate.toDate(),
selectionInfo.start,
firstSwimlane._id,
function (error, result) {
if (error) {

View file

@ -0,0 +1,43 @@
.calendar-view .fc {
--fc-button-text-color: #333;
--fc-button-bg-color: #f5f5f5;
--fc-button-border-color: rgba(0, 0, 0, 0.2);
--fc-button-hover-bg-color: #e6e6e6;
--fc-button-hover-border-color: rgba(0, 0, 0, 0.25);
--fc-button-active-bg-color: #d9d9d9;
--fc-button-active-border-color: rgba(0, 0, 0, 0.3);
}
.calendar-view .fc .fc-button-primary {
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
background-image: linear-gradient(to bottom, #fff 0%, #e6e6e6 100%);
}
.calendar-view .fc .fc-button-primary:focus,
.calendar-view .fc .fc-button-primary:not(:disabled).fc-button-active,
.calendar-view .fc .fc-button-primary:not(:disabled):active {
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),
0 1px 2px rgba(0, 0, 0, 0.05);
}
.calendar-create-close {
min-height: auto !important;
min-width: auto !important;
width: 32px;
height: 32px;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
color: #666 !important;
opacity: 0.8;
}
.calendar-create-close:hover,
.calendar-create-close:focus {
background: transparent !important;
color: #111 !important;
opacity: 1;
}

View file

@ -1077,7 +1077,7 @@ template(name="cardMorePopup")
option(value="{{_id}}") {{title}}
br
| {{_ 'added'}}
span.date(title=card.createdAt) {{ moment createdAt 'LLL' }}
span.date(title=card.createdAt) {{ displayDate createdAt 'LLL' }}
if currentUser.isBoardAdmin
a.js-delete(title="{{_ 'card-delete-notice'}}") {{_ 'delete'}}

View file

@ -92,7 +92,7 @@ template(name="editChecklistItemForm")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form(title="{{_ 'close-edit-checklist-item'}}")
span(title=createdAt) {{ moment createdAt }}
span(title=createdAt) {{ displayDate createdAt }}
if canModifyCard
a.js-delete-checklist-item {{_ "delete"}}...
a.js-convert-checklist-item-to-card

View file

@ -51,7 +51,7 @@ template(name="editSubtaskItemForm")
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-subtask-item-form(type="submit") {{_ 'save'}}
a.js-close-inlined-form
span(title=createdAt) {{ moment createdAt }}
span(title=createdAt) {{ displayDate createdAt }}
if canModifyCard
if currentUser.isBoardAdmin
a.js-delete-subtask-item {{_ "delete"}}...

View file

@ -16,7 +16,7 @@ template(name="listHeader")
span.cardCount {{cardsCount}}
if isMiniScreen
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
title="{{ displayDate modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
@ -37,7 +37,7 @@ template(name="listHeader")
i.fa.fa-caret-down
div(class="{{#if collapsed}}list-rotated{{/if}}")
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
title="{{ displayDate modifiedAt 'LLL' }}"
class="{{#unless collapsed}}{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}{{/unless}}")
+viewer
= title
@ -193,7 +193,7 @@ template(name="listMorePopup")
i.fa(class="{{#if currentBoard.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
span.date(title=list.createdAt) {{ displayDate createdAt 'LLL' }}
//unless currentUser.isWorker
//
if currentUser.isBoardAdmin

View file

@ -96,7 +96,7 @@ template(name="myCards")
| {{labelName board label}}
td
if card.dueAt
| {{ moment card.dueAt 'LLL' }}
| {{ displayDate card.dueAt 'LLL' }}
template(name="myCardsViewChangePopup")
if currentUser

View file

@ -1,4 +1,5 @@
import { ReactiveCache } from '/imports/reactiveCache';
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
Template.notification.events({
'click .read-status .materialCheckBox'() {
@ -38,9 +39,15 @@ Template.notification.helpers({
const user = ReactiveCache.getCurrentUser();
if (!user) return '';
const dateFormat = user.getDateFormat ? user.getDateFormat() : 'L';
const timeFormat = user.getTimeFormat ? user.getTimeFormat() : 'LT';
const dateObj = new Date(activity.createdAt);
if (Number.isNaN(dateObj.getTime())) return '';
return moment(activity.createdAt).format(`${dateFormat} ${timeFormat}`);
const dateFormat = user.getDateFormat ? user.getDateFormat() : 'YYYY-MM-DD';
const datePart = formatDateByUserPreference(dateObj, dateFormat, false);
const timePart = dateObj.toLocaleTimeString([], {
hour: 'numeric',
minute: '2-digit',
});
return `${datePart} ${timePart}`.trim();
},
});

View file

@ -190,9 +190,9 @@ template(name="orgRow")
else
td <s>{{ orgData.orgWebsite }}</s>
if orgData.orgIsActive
td {{ moment orgData.createdAt 'LLL' }}
td {{ displayDate orgData.createdAt 'LLL' }}
else
td <s>{{ moment orgData.createdAt 'LLL' }}</s>
td <s>{{ displayDate orgData.createdAt 'LLL' }}</s>
td
if orgData.orgIsActive
| {{_ 'yes'}}
@ -224,9 +224,9 @@ template(name="teamRow")
else
td <s>{{ teamData.teamWebsite }}</s>
if teamData.teamIsActive
td {{ moment teamData.createdAt 'LLL' }}
td {{ displayDate teamData.createdAt 'LLL' }}
else
td <s>{{ moment teamData.createdAt 'LLL' }}</s>
td <s>{{ displayDate teamData.createdAt 'LLL' }}</s>
td
if teamData.teamIsActive
| {{_ 'yes'}}
@ -284,9 +284,9 @@ template(name="peopleRow")
span.text-green.js-toggle-lock-status.emoji-icon(data-user-id=userData._id, data-is-locked="false", title="{{_ 'accounts-lockout-user-unlocked'}}")
i.fa.fa-unlock
if userData.loginDisabled
td <s>{{ moment userData.createdAt 'LLL' }}</s>
td <s>{{ displayDate userData.createdAt 'LLL' }}</s>
else
td {{ moment userData.createdAt 'LLL' }}
td {{ displayDate userData.createdAt 'LLL' }}
if userData.loginDisabled
td
input.selectUserChkBox(type="checkbox", disabled="disabled", id="{{userData._id}}")

View file

@ -18,9 +18,9 @@ import {
cronMigrationEtaSeconds,
cronMigrationElapsedSeconds,
cronMigrationCurrentNumber,
cronMigrationCurrentName
cronMigrationCurrentName,
} from '/imports/cronMigrationClient';
import { format } from '/imports/lib/dateUtils';
BlazeComponent.extendComponent({
onCreated() {
@ -66,7 +66,6 @@ BlazeComponent.extendComponent({
}
},
setError(error) {
this.error.set(error);
},
@ -82,7 +81,9 @@ BlazeComponent.extendComponent({
return this.accountSetting && this.accountSetting.get();
},
isTableVisibilityModeSetting() {
return this.tableVisibilityModeSetting && this.tableVisibilityModeSetting.get();
return (
this.tableVisibilityModeSetting && this.tableVisibilityModeSetting.get()
);
},
isAnnouncementSetting() {
return this.announcementSetting && this.announcementSetting.get();
@ -174,7 +175,7 @@ BlazeComponent.extendComponent({
const steps = cronMigrationSteps.get() || [];
return steps.map((step, idx) => ({
...step,
index: idx + 1
index: idx + 1,
}));
},
@ -237,7 +238,9 @@ BlazeComponent.extendComponent({
isUpdatingMigrationDropdown() {
const status = this.migrationStatus();
return status && status.startsWith('Updating Select Migration dropdown menu');
return (
status && status.startsWith('Updating Select Migration dropdown menu')
);
},
migrationErrors() {
@ -251,7 +254,7 @@ BlazeComponent.extendComponent({
formatDateTime(date) {
if (!date) return '';
return moment(date).format('YYYY-MM-DD HH:mm:ss');
return format(date, 'YYYY-MM-DD HH:mm:ss');
},
formatDurationSeconds(seconds) {
@ -331,18 +334,22 @@ BlazeComponent.extendComponent({
});
} else {
// Run specific migration
Meteor.call('cron.startSpecificMigration', selectedIndex - 1, (error, result) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else if (result && result.skipped) {
cronIsMigrating.set(false);
cronMigrationStatus.set(TAPi18n.__('migration-not-needed'));
alert(TAPi18n.__('migration-not-needed'));
} else {
alert(TAPi18n.__('migration-started'));
}
});
Meteor.call(
'cron.startSpecificMigration',
selectedIndex - 1,
(error, result) => {
this.setLoading(false);
if (error) {
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
} else if (result && result.skipped) {
cronIsMigrating.set(false);
cronMigrationStatus.set(TAPi18n.__('migration-not-needed'));
alert(TAPi18n.__('migration-not-needed'));
} else {
alert(TAPi18n.__('migration-started'));
}
},
);
}
},
@ -490,9 +497,7 @@ BlazeComponent.extendComponent({
checkField(selector) {
const value = $(selector).val();
if (!value || value.trim() === '') {
$(selector)
.parents('li.smtp-form')
.addClass('has-error');
$(selector).parents('li.smtp-form').addClass('has-error');
throw Error('blank field');
} else {
return value;
@ -514,7 +519,8 @@ BlazeComponent.extendComponent({
},
toggleForgotPassword() {
this.setLoading(true);
const forgotPasswordClosed = ReactiveCache.getCurrentSetting().disableForgotPassword;
const forgotPasswordClosed =
ReactiveCache.getCurrentSetting().disableForgotPassword;
Settings.update(ReactiveCache.getCurrentSetting()._id, {
$set: { disableForgotPassword: !forgotPasswordClosed },
});
@ -522,7 +528,8 @@ BlazeComponent.extendComponent({
},
toggleRegistration() {
this.setLoading(true);
const registrationClosed = ReactiveCache.getCurrentSetting().disableRegistration;
const registrationClosed =
ReactiveCache.getCurrentSetting().disableRegistration;
Settings.update(ReactiveCache.getCurrentSetting()._id, {
$set: { disableRegistration: !registrationClosed },
});
@ -629,11 +636,11 @@ BlazeComponent.extendComponent({
.join(',')
.split(',');
const boardsToInvite = [];
$('.js-toggle-board-choose .materialCheckBox.is-checked').each(function() {
$('.js-toggle-board-choose .materialCheckBox.is-checked').each(function () {
boardsToInvite.push($(this).data('id'));
});
const validEmails = [];
emails.forEach(email => {
emails.forEach((email) => {
if (email && SimpleSchema.RegEx.Email.test(email.trim())) {
validEmails.push(email.trim());
}
@ -656,12 +663,8 @@ BlazeComponent.extendComponent({
try {
const host = this.checkField('#mail-server-host');
const port = this.checkField('#mail-server-port');
const username = $('#mail-server-username')
.val()
.trim();
const password = $('#mail-server-password')
.val()
.trim();
const username = $('#mail-server-username').val().trim();
const password = $('#mail-server-password').val().trim();
const from = this.checkField('#mail-server-from');
const tls = $('#mail-server-tls.is-checked').length > 0;
Settings.update(ReactiveCache.getCurrentSetting()._id, {
@ -686,21 +689,37 @@ BlazeComponent.extendComponent({
$('li').removeClass('has-error');
const productName = ($('#product-name').val() || '').trim();
const customLoginLogoImageUrl = ($('#custom-login-logo-image-url').val() || '').trim();
const customLoginLogoLinkUrl = ($('#custom-login-logo-link-url').val() || '').trim();
const customLoginLogoImageUrl = (
$('#custom-login-logo-image-url').val() || ''
).trim();
const customLoginLogoLinkUrl = (
$('#custom-login-logo-link-url').val() || ''
).trim();
const customHelpLinkUrl = ($('#custom-help-link-url').val() || '').trim();
const textBelowCustomLoginLogo = ($('#text-below-custom-login-logo').val() || '').trim();
const automaticLinkedUrlSchemes = ($('#automatic-linked-url-schemes').val() || '').trim();
const customTopLeftCornerLogoImageUrl = ($('#custom-top-left-corner-logo-image-url').val() || '').trim();
const customTopLeftCornerLogoLinkUrl = ($('#custom-top-left-corner-logo-link-url').val() || '').trim();
const customTopLeftCornerLogoHeight = ($('#custom-top-left-corner-logo-height').val() || '').trim();
const textBelowCustomLoginLogo = (
$('#text-below-custom-login-logo').val() || ''
).trim();
const automaticLinkedUrlSchemes = (
$('#automatic-linked-url-schemes').val() || ''
).trim();
const customTopLeftCornerLogoImageUrl = (
$('#custom-top-left-corner-logo-image-url').val() || ''
).trim();
const customTopLeftCornerLogoLinkUrl = (
$('#custom-top-left-corner-logo-link-url').val() || ''
).trim();
const customTopLeftCornerLogoHeight = (
$('#custom-top-left-corner-logo-height').val() || ''
).trim();
const oidcBtnText = ($('#oidcBtnTextvalue').val() || '').trim();
const mailDomainName = ($('#mailDomainNamevalue').val() || '').trim();
const legalNotice = ($('#legalNoticevalue').val() || '').trim();
const hideLogoChange = $('input[name=hideLogo]:checked').val() === 'true';
const hideCardCounterListChange = $('input[name=hideCardCounterList]:checked').val() === 'true';
const hideBoardMemberListChange = $('input[name=hideBoardMemberList]:checked').val() === 'true';
const hideCardCounterListChange =
$('input[name=hideCardCounterList]:checked').val() === 'true';
const hideBoardMemberListChange =
$('input[name=hideBoardMemberList]:checked').val() === 'true';
const displayAuthenticationMethod =
$('input[name=displayAuthenticationMethod]:checked').val() === 'true';
const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
@ -740,7 +759,9 @@ BlazeComponent.extendComponent({
toggleSupportPage() {
this.setLoading(true);
const supportPageEnabled = !$('.js-toggle-support .materialCheckBox').hasClass('is-checked');
const supportPageEnabled = !$(
'.js-toggle-support .materialCheckBox',
).hasClass('is-checked');
$('.js-toggle-support .materialCheckBox').toggleClass('is-checked');
$('.support-content').toggleClass('hide');
Settings.update(Settings.findOne()._id, {
@ -751,7 +772,9 @@ BlazeComponent.extendComponent({
toggleSupportPublic() {
this.setLoading(true);
const supportPagePublic = !$('.js-toggle-support-public .materialCheckBox').hasClass('is-checked');
const supportPagePublic = !$(
'.js-toggle-support-public .materialCheckBox',
).hasClass('is-checked');
$('.js-toggle-support-public .materialCheckBox').toggleClass('is-checked');
Settings.update(Settings.findOne()._id, {
$set: { supportPagePublic },
@ -761,7 +784,9 @@ BlazeComponent.extendComponent({
toggleCustomHead() {
this.setLoading(true);
const customHeadEnabled = !$('.js-toggle-custom-head .materialCheckBox').hasClass('is-checked');
const customHeadEnabled = !$(
'.js-toggle-custom-head .materialCheckBox',
).hasClass('is-checked');
$('.js-toggle-custom-head .materialCheckBox').toggleClass('is-checked');
$('.custom-head-settings').toggleClass('hide');
Settings.update(ReactiveCache.getCurrentSetting()._id, {
@ -772,7 +797,9 @@ BlazeComponent.extendComponent({
toggleCustomManifest() {
this.setLoading(true);
const customManifestEnabled = !$('.js-toggle-custom-manifest .materialCheckBox').hasClass('is-checked');
const customManifestEnabled = !$(
'.js-toggle-custom-manifest .materialCheckBox',
).hasClass('is-checked');
$('.js-toggle-custom-manifest .materialCheckBox').toggleClass('is-checked');
$('.custom-manifest-settings').toggleClass('hide');
Settings.update(ReactiveCache.getCurrentSetting()._id, {
@ -829,7 +856,9 @@ BlazeComponent.extendComponent({
const errorMsg = e.message;
// If error is "unexpected non-whitespace character after JSON data"
if (errorMsg.includes('unexpected non-whitespace character after JSON data')) {
if (
errorMsg.includes('unexpected non-whitespace character after JSON data')
) {
try {
// Try to find and extract valid JSON by finding matching braces/brackets
const trimmed = content.trim();
@ -896,8 +925,12 @@ BlazeComponent.extendComponent({
toggleCustomAssetLinks() {
this.setLoading(true);
const customAssetLinksEnabled = !$('.js-toggle-custom-assetlinks .materialCheckBox').hasClass('is-checked');
$('.js-toggle-custom-assetlinks .materialCheckBox').toggleClass('is-checked');
const customAssetLinksEnabled = !$(
'.js-toggle-custom-assetlinks .materialCheckBox',
).hasClass('is-checked');
$('.js-toggle-custom-assetlinks .materialCheckBox').toggleClass(
'is-checked',
);
$('.custom-assetlinks-settings').toggleClass('hide');
Settings.update(ReactiveCache.getCurrentSetting()._id, {
$set: { customAssetLinksEnabled },
@ -978,8 +1011,10 @@ BlazeComponent.extendComponent({
'click button.js-save': this.saveMailServerInfo,
'click button.js-send-smtp-test-email': this.sendSMTPTestEmail,
'click a.js-toggle-hide-logo': this.toggleHideLogo,
'click a.js-toggle-hide-card-counter-list': this.toggleHideCardCounterList,
'click a.js-toggle-hide-board-member-list': this.toggleHideBoardMemberList,
'click a.js-toggle-hide-card-counter-list':
this.toggleHideCardCounterList,
'click a.js-toggle-hide-board-member-list':
this.toggleHideBoardMemberList,
'click button.js-save-layout': this.saveLayout,
'click a.js-toggle-support': this.toggleSupportPage,
'click a.js-toggle-support-public': this.toggleSupportPublic,
@ -988,9 +1023,10 @@ BlazeComponent.extendComponent({
'click a.js-toggle-custom-manifest': this.toggleCustomManifest,
'click button.js-custom-head-save': this.saveCustomHeadSettings,
'click a.js-toggle-custom-assetlinks': this.toggleCustomAssetLinks,
'click button.js-custom-assetlinks-save': this.saveCustomAssetLinksSettings,
'click a.js-toggle-display-authentication-method': this
.toggleDisplayAuthenticationMethod,
'click button.js-custom-assetlinks-save':
this.saveCustomAssetLinksSettings,
'click a.js-toggle-display-authentication-method':
this.toggleDisplayAuthenticationMethod,
},
];
},
@ -1018,15 +1054,23 @@ BlazeComponent.extendComponent({
// Brute force lockout settings method moved to lockedUsersBody.js
allowEmailChange() {
return AccountSettings.findOne('accounts-allowEmailChange')?.booleanValue || false;
return (
AccountSettings.findOne('accounts-allowEmailChange')?.booleanValue ||
false
);
},
allowUserNameChange() {
return AccountSettings.findOne('accounts-allowUserNameChange')?.booleanValue || false;
return (
AccountSettings.findOne('accounts-allowUserNameChange')?.booleanValue ||
false
);
},
allowUserDelete() {
return AccountSettings.findOne('accounts-allowUserDelete')?.booleanValue || false;
return (
AccountSettings.findOne('accounts-allowUserDelete')?.booleanValue || false
);
},
// Lockout settings helper methods moved to lockedUsersBody.js
@ -1054,7 +1098,8 @@ BlazeComponent.extendComponent({
'click button.js-accounts-save': this.saveAccountsChange,
},
{
'click button.js-all-boards-hide-activities': this.allBoardsHideActivities,
'click button.js-all-boards-hide-activities':
this.allBoardsHideActivities,
},
];
},
@ -1069,7 +1114,9 @@ BlazeComponent.extendComponent({
});
},
allowPrivateOnly() {
return TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
return TableVisibilityModeSettings.findOne(
'tableVisibilityMode-allowPrivateOnly',
).booleanValue;
},
allBoardsHideActivities() {
Meteor.call('setAllBoardsHideActivities', (err, ret) => {
@ -1091,10 +1138,12 @@ BlazeComponent.extendComponent({
events() {
return [
{
'click button.js-tableVisibilityMode-save': this.saveTableVisibilityChange,
'click button.js-tableVisibilityMode-save':
this.saveTableVisibilityChange,
},
{
'click button.js-all-boards-hide-activities': this.allBoardsHideActivities,
'click button.js-all-boards-hide-activities':
this.allBoardsHideActivities,
},
];
},
@ -1114,9 +1163,7 @@ BlazeComponent.extendComponent({
},
saveMessage() {
const message = $('#admin-announcement')
.val()
.trim();
const message = $('#admin-announcement').val().trim();
Announcements.update(Announcements.findOne()._id, {
$set: { body: message },
});
@ -1162,18 +1209,14 @@ BlazeComponent.extendComponent({
saveAccessibility() {
this.setLoading(true);
const title = $('#admin-accessibility-title')
.val()
.trim();
const content = $('#admin-accessibility-content')
.val()
.trim();
const title = $('#admin-accessibility-title').val().trim();
const content = $('#admin-accessibility-content').val().trim();
try {
AccessibilitySettings.update(AccessibilitySettings.findOne()._id, {
$set: {
title: title,
body: content
body: content,
},
});
} catch (e) {
@ -1209,7 +1252,7 @@ BlazeComponent.extendComponent({
},
}).register('accessibilitySettings');
Template.selectAuthenticationMethod.onCreated(function() {
Template.selectAuthenticationMethod.onCreated(function () {
this.authenticationMethods = new ReactiveVar([]);
Meteor.call('getAuthenticationsEnabled', (_, result) => {
@ -1220,8 +1263,8 @@ Template.selectAuthenticationMethod.onCreated(function() {
{ value: 'password' },
// Gets only the authentication methods availables
...Object.entries(result)
.filter(e => e[1])
.map(e => ({ value: e[0] })),
.filter((e) => e[1])
.map((e) => ({ value: e[0] })),
]);
}
});
@ -1244,4 +1287,3 @@ Template.selectSpinnerName.helpers({
return Template.instance().data.spinnerName === match;
},
});

View file

@ -20,7 +20,7 @@ template(name="archivesSidebar")
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
| | {{ moment this.archivedAt 'LLL' }}
| | {{ displayDate this.archivedAt 'LLL' }}
br
a.js-restore-card {{_ 'restore'}}
if currentUser.isBoardAdmin
@ -51,7 +51,7 @@ template(name="archivesSidebar")
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
| | {{ moment this.archivedAt 'LLL' }}
| | {{ displayDate this.archivedAt 'LLL' }}
br
a.js-restore-list {{_ 'restore'}}
if currentUser.isBoardAdmin
@ -80,7 +80,7 @@ template(name="archivesSidebar")
p.quiet
if this.archivedAt
| {{_ 'archived-at' }}
| | {{ moment this.archivedAt 'LLL' }}
| | {{ displayDate this.archivedAt 'LLL' }}
br
a.js-restore-swimlane {{_ 'restore'}}
if currentUser.isBoardAdmin

View file

@ -7,25 +7,25 @@ import {
} from '/imports/lib/customHeadDefaults';
import { Blaze } from 'meteor/blaze';
import { Session } from 'meteor/session';
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar
import {
formatDateTime,
formatDate,
formatTime,
getISOWeek,
isValidDate,
isBefore,
isAfter,
isSame,
add,
subtract,
startOf,
endOf,
format,
parseDate,
now,
createDate,
fromNow,
calendar,
} from '/imports/lib/dateUtils';
Blaze.registerHelper('currentBoard', () => {
@ -85,7 +85,7 @@ Blaze.registerHelper('currentUser', () => {
return ret;
});
Blaze.registerHelper('getUser', userId => ReactiveCache.getUser(userId));
Blaze.registerHelper('getUser', (userId) => ReactiveCache.getUser(userId));
Blaze.registerHelper('concat', (...args) => args.slice(0, -1).join(''));
@ -101,23 +101,17 @@ Blaze.registerHelper('isTouchScreenOrShowDesktopDragHandles', () =>
Utils.isTouchScreenOrShowDesktopDragHandles(),
);
Blaze.registerHelper('moment', (...args) => {
Blaze.registerHelper('displayDate', (...args) => {
args.pop(); // hash
const [date, formatStr] = args;
return format(new Date(date), formatStr ?? 'LLLL');
});
Blaze.registerHelper('canModifyCard', () =>
Utils.canModifyCard(),
);
Blaze.registerHelper('canModifyCard', () => Utils.canModifyCard());
Blaze.registerHelper('canMoveCard', () =>
Utils.canMoveCard(),
);
Blaze.registerHelper('canMoveCard', () => Utils.canMoveCard());
Blaze.registerHelper('canModifyBoard', () =>
Utils.canModifyBoard(),
);
Blaze.registerHelper('canModifyBoard', () => Utils.canModifyBoard());
Blaze.registerHelper('add', (a, b) => a + b);

View file

@ -167,9 +167,9 @@
"board-background-image-url": "Hintergrundbild URL",
"add-background-image": "Hintergrundbild hinzufügen",
"remove-background-image": "Hintergrundbild entfernen",
"show-at-all-boards-page" : "Auf der \"Alle Boards\" Seite anzeigen",
"board-info-on-my-boards" : "Einstellungen für Alle Boards",
"boardInfoOnMyBoardsPopup-title" : "Einstellungen für Alle Boards",
"show-at-all-boards-page": "Auf der \"Alle Boards\" Seite anzeigen",
"board-info-on-my-boards": "Einstellungen für Alle Boards",
"boardInfoOnMyBoardsPopup-title": "Einstellungen für Alle Boards",
"boardInfoOnMyBoards-title": "Einstellungen für Alle Boards",
"show-card-counter-per-list": "Zeige Kartenanzahl pro Liste",
"show-board_members-avatar": "Zeige Profilbilder der Board-Mitglieder",
@ -767,7 +767,7 @@
"accounts-allowEmailChange": "Ändern der E-Mailadresse erlauben",
"accounts-allowUserNameChange": "Ändern des Benutzernamens erlauben",
"tableVisibilityMode-allowPrivateOnly": "Board-Sichtbarkeit: Erlaube ausschließlich private Boards",
"tableVisibilityMode" : "Sichtbarkeit der Boards",
"tableVisibilityMode": "Sichtbarkeit der Boards",
"createdAt": "Erstellt am",
"modifiedAt": "Geändert am",
"verified": "Geprüft",
@ -1048,6 +1048,10 @@
"person": "Person",
"my-cards": "Meine Karten",
"card": "Karte",
"today": "Heute",
"day": "Tag",
"week": "Woche",
"month": "Monat",
"list": "Liste",
"board": "Board",
"context-separator": "/",

View file

@ -167,9 +167,9 @@
"board-background-image-url": "Background Image URL",
"add-background-image": "Add Background Image",
"remove-background-image": "Remove Background Image",
"show-at-all-boards-page" : "Show at All Boards page",
"board-info-on-my-boards" : "All Boards Settings",
"boardInfoOnMyBoardsPopup-title" : "All Boards Settings",
"show-at-all-boards-page": "Show at All Boards page",
"board-info-on-my-boards": "All Boards Settings",
"boardInfoOnMyBoardsPopup-title": "All Boards Settings",
"boardInfoOnMyBoards-title": "All Boards Settings",
"show-card-counter-per-list": "Show card count per list",
"show-board_members-avatar": "Show Board members avatars",
@ -767,7 +767,7 @@
"accounts-allowEmailChange": "Allow Email Change",
"accounts-allowUserNameChange": "Allow Username Change",
"tableVisibilityMode-allowPrivateOnly": "Boards visibility: Allow private boards only",
"tableVisibilityMode" : "Boards visibility",
"tableVisibilityMode": "Boards visibility",
"createdAt": "Created at",
"modifiedAt": "Modified at",
"verified": "Verified",
@ -1048,6 +1048,10 @@
"person": "Person",
"my-cards": "My Cards",
"card": "Card",
"today": "Today",
"day": "Day",
"week": "Week",
"month": "Month",
"list": "List",
"board": "Board",
"context-separator": "/",

View file

@ -167,9 +167,9 @@
"board-background-image-url": "URL de la imagen de fondo",
"add-background-image": "Añadir imagen de fondo",
"remove-background-image": "Quitar imagen de fondo",
"show-at-all-boards-page" : "Mostrar todos los tableros",
"board-info-on-my-boards" : "Configuración de todos los tableros",
"boardInfoOnMyBoardsPopup-title" : "Configuración de todos los tableros",
"show-at-all-boards-page": "Mostrar todos los tableros",
"board-info-on-my-boards": "Configuración de todos los tableros",
"boardInfoOnMyBoardsPopup-title": "Configuración de todos los tableros",
"boardInfoOnMyBoards-title": "Configuración de todos los tableros",
"show-card-counter-per-list": "Mostrar el contador de tarjetas por lista",
"show-board_members-avatar": "Mostrar los avatares de los miembros del tablero",
@ -767,7 +767,7 @@
"accounts-allowEmailChange": "Permitir cambiar el correo electrónico",
"accounts-allowUserNameChange": "Permitir cambiar el nombre de usuario",
"tableVisibilityMode-allowPrivateOnly": "Boards visibility: Allow private boards only",
"tableVisibilityMode" : "Boards visibility",
"tableVisibilityMode": "Boards visibility",
"createdAt": "Fecha de alta",
"modifiedAt": "Modified at",
"verified": "Verificado",
@ -1048,6 +1048,10 @@
"person": "Persona",
"my-cards": "Mis Tarjetas",
"card": "Tarjeta",
"today": "Hoy",
"day": "Día",
"week": "Semana",
"month": "Mes",
"list": "Lista",
"board": "Tablero",
"context-separator": "/",

View file

@ -43,7 +43,11 @@ export function formatDate(date) {
* @param {boolean} includeTime - Whether to include time (HH:MM)
* @returns {string} Formatted date string
*/
export function formatDateByUserPreference(date, format = 'YYYY-MM-DD', includeTime = true) {
export function formatDateByUserPreference(
date,
format = 'YYYY-MM-DD',
includeTime = true,
) {
const d = new Date(date);
if (isNaN(d.getTime())) return '';
@ -108,7 +112,7 @@ export function getISOWeek(date) {
const firstThursday = target.valueOf();
target.setMonth(0, 1);
if (target.getDay() !== 4) {
target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
target.setMonth(0, 1 + ((4 - target.getDay() + 7) % 7));
}
return 1 + Math.ceil((firstThursday - target) / 604800000); // 604800000 = 7 * 24 * 3600 * 1000
@ -141,16 +145,31 @@ export function isBefore(date1, date2, unit = 'millisecond') {
case 'year':
return d1.getFullYear() < d2.getFullYear();
case 'month':
return d1.getFullYear() < d2.getFullYear() ||
(d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth());
return (
d1.getFullYear() < d2.getFullYear() ||
(d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth())
);
case 'day':
return d1.getFullYear() < d2.getFullYear() ||
(d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth()) ||
(d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() < d2.getDate());
return (
d1.getFullYear() < d2.getFullYear() ||
(d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() < d2.getMonth()) ||
(d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() < d2.getDate())
);
case 'hour':
return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60 * 60)) < Math.floor(d2.getTime() / (1000 * 60 * 60));
return (
d1.getTime() < d2.getTime() &&
Math.floor(d1.getTime() / (1000 * 60 * 60)) <
Math.floor(d2.getTime() / (1000 * 60 * 60))
);
case 'minute':
return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60)) < Math.floor(d2.getTime() / (1000 * 60));
return (
d1.getTime() < d2.getTime() &&
Math.floor(d1.getTime() / (1000 * 60)) <
Math.floor(d2.getTime() / (1000 * 60))
);
default:
return d1.getTime() < d2.getTime();
}
@ -184,13 +203,25 @@ export function isSame(date1, date2, unit = 'millisecond') {
case 'year':
return d1.getFullYear() === d2.getFullYear();
case 'month':
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
return (
d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth()
);
case 'day':
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
return (
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
);
case 'hour':
return Math.floor(d1.getTime() / (1000 * 60 * 60)) === Math.floor(d2.getTime() / (1000 * 60 * 60));
return (
Math.floor(d1.getTime() / (1000 * 60 * 60)) ===
Math.floor(d2.getTime() / (1000 * 60 * 60))
);
case 'minute':
return Math.floor(d1.getTime() / (1000 * 60)) === Math.floor(d2.getTime() / (1000 * 60));
return (
Math.floor(d1.getTime() / (1000 * 60)) ===
Math.floor(d2.getTime() / (1000 * 60))
);
default:
return d1.getTime() === d2.getTime();
}
@ -350,6 +381,8 @@ export function format(date, format = 'L') {
return `${year}-${month}-${day}`;
case 'YYYY-MM-DD HH:mm':
return `${year}-${month}-${day} ${hours}:${minutes}`;
case 'YYYY-MM-DD HH:mm:ss':
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
case 'HH:mm':
return `${hours}:${minutes}`;
default:
@ -384,7 +417,7 @@ export function parseDate(dateString, formats = [], strict = true) {
'DD/MM/YYYY HH:mm',
'DD/MM/YYYY',
'DD-MM-YYYY HH:mm',
'DD-MM-YYYY'
'DD-MM-YYYY',
];
const allFormats = [...formats, ...commonFormats];
@ -408,12 +441,12 @@ export function parseDate(dateString, formats = [], strict = true) {
function parseWithFormat(dateString, format) {
// Simple format parsing - can be extended as needed
const formatMap = {
'YYYY': '\\d{4}',
'MM': '\\d{2}',
'DD': '\\d{2}',
'HH': '\\d{2}',
'mm': '\\d{2}',
'ss': '\\d{2}'
YYYY: '\\d{4}',
MM: '\\d{2}',
DD: '\\d{2}',
HH: '\\d{2}',
mm: '\\d{2}',
ss: '\\d{2}',
};
let regex = format;
@ -425,11 +458,21 @@ function parseWithFormat(dateString, format) {
if (!match) return null;
const groups = match.slice(1);
let year, month, day, hour = 0, minute = 0, second = 0;
let year,
month,
day,
hour = 0,
minute = 0,
second = 0;
let groupIndex = 0;
for (let i = 0; i < format.length; i++) {
if (format[i] === 'Y' && format[i + 1] === 'Y' && format[i + 2] === 'Y' && format[i + 3] === 'Y') {
if (
format[i] === 'Y' &&
format[i + 1] === 'Y' &&
format[i + 2] === 'Y' &&
format[i + 3] === 'Y'
) {
year = parseInt(groups[groupIndex++]);
i += 3;
} else if (format[i] === 'M' && format[i + 1] === 'M') {
@ -501,11 +544,15 @@ export function fromNow(date, now = new Date()) {
const diffYears = Math.floor(diffDays / 365);
if (diffSeconds < 60) return 'a few seconds ago';
if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
if (diffMinutes < 60)
return `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''} ago`;
if (diffHours < 24)
return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks !== 1 ? 's' : ''} ago`;
if (diffMonths < 12) return `${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`;
if (diffWeeks < 4)
return `${diffWeeks} week${diffWeeks !== 1 ? 's' : ''} ago`;
if (diffMonths < 12)
return `${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`;
return `${diffYears} year${diffYears !== 1 ? 's' : ''} ago`;
}

20
package-lock.json generated
View file

@ -176,12 +176,12 @@
"integrity": "sha512-DAz2ZDtUn7dd0Zol1wdKkhSG4U+OwlDcGzeu1t8XwWh9SKtfTaIaMYTqTvJfAg2B3ilIHp2k64c5mqOiRq5lWQ=="
},
"@wekanteam/exceljs": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@wekanteam/exceljs/-/exceljs-4.6.0.tgz",
"integrity": "sha512-R5var++3oPGTbfPrswOuQQEP8XsookaErND1vHkVkpnCuirCAcmEiLLdcakAJHFQVwxDANpN4lYzS1qSXSXCPg==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@wekanteam/exceljs/-/exceljs-4.5.1.tgz",
"integrity": "sha512-qEWJKSjExu7YJ07YSp3BVj8UvVz1hQR7yh18XdxOn7Wu41wXjbcFpXuMr8GNtj11mE33z5xdUyADcrKLJVfVLQ==",
"requires": {
"archiver": "^5.0.0",
"dayjs": "^1.8.34",
"dayjs": "^1.11.19",
"fast-csv": "^4.3.1",
"jszip": "^3.10.1",
"readable-stream": "^3.6.0",
@ -2235,9 +2235,9 @@
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz",
"integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==",
"requires": {
"brace-expansion": "^1.1.7"
}
@ -2489,9 +2489,9 @@
}
},
"minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.7.tgz",
"integrity": "sha512-FjiwU9HaHW6YB3H4a1sFudnv93lvydNjz2lmyUXR6IwKhGI+bgL3SOZrBGn6kvvX2pJvhEkGSGjyTHN47O4rqA==",
"requires": {
"brace-expansion": "^2.0.1"
}

View file

@ -1,57 +1,51 @@
[FullCalendar](http://fullcalendar.io/) JQuery plugin packaged for Meteor 1.0
[FullCalendar](https://fullcalendar.io/) packaged for Wekan as a Blaze wrapper.
### Instalation ###
### Installation
meteor add rzymek:fullcalendar
This package is bundled in Wekan (`wekan-fullcalendar`).
### Usage ###
### Usage
{{> fullcalendar }}
```handlebars
{{> fullcalendar calendarOptions}}
```
Options to FullCalendar can be passed as attributes:
Options can be passed directly from a helper:
{{> fullcalendar defaultView='agendaWeek'}}
If you want to have options defined in JS (or have them reactive), you can do:
```js
Template.example.helpers({
calendarOptions() {
return {
id: 'myCalendar',
initialView: 'dayGridMonth',
headerToolbar: {
left: 'title today prev,next',
center: 'timeGridDay,timeGridWeek,dayGridMonth,listMonth',
right: '',
},
events(fetchInfo, successCallback) {
successCallback([]);
},
};
},
});
```
<template name="example">
{{>fullcalendar options}}
</template>
### Compatibility notes
Template.example.helpers({
options: function() {
return {
defaultView: 'basicWeek'
};
}
});
- Uses FullCalendar v5 modules (`@fullcalendar/*`) and no longer uses jQuery plugin APIs.
- Legacy options are mapped for compatibility:
- `defaultView` -> `initialView`
- `header` -> `headerToolbar`
To access the `.fullcalendar` method assign an `id` or a `class` first
### Refetching events
{{> fullcalendar id="myCalendar" ...}}
If you provide an `id`, the wrapper stores the calendar instance on the container
element as `_wekanCalendar`:
Then you can for example do
$('#myCalendar').fullCalendar('refetchEvents');
### Updating fullcalendar ###
To update fullcalendar version run
./update.sh
This will update to the newest fullcalendar's tag.
To update to a specific version do
./update.sh 2.2.6
If you want me to publish a new package version just [create an issue](https://github.com/rzymek/meteor-fullcalendar/issues/new).
In case you can't wait to use a new fullcalendar version in your project, you can update the package locally:
cd your_meteor_project
mkdir -p packages
git clone https://github.com/rzymek/meteor-fullcalendar packages/rzymek:fullcalendar
./packages/rzymek:fullcalendar/update.sh
After the desired version gets published just remove the local package:
rm -r packages/rzymek:fullcalendar
```js
const el = document.getElementById('myCalendar');
if (el && el._wekanCalendar) {
el._wekanCalendar.refetchEvents();
}
```

View file

@ -1,21 +1,30 @@
Package.describe({
name: 'wekan-fullcalendar',
summary: "Full-sized drag & drop event calendar (jQuery plugin)",
version: "3.10.5",
git: "https://github.com/fullcalendar/fullcalendar.git"
name: 'wekan-fullcalendar',
summary: 'Full-sized drag & drop event calendar (jQuery plugin)',
version: '5.11.5',
git: 'https://github.com/fullcalendar/fullcalendar.git',
});
Npm.depends({
'@fullcalendar/core': '5.11.5',
'@fullcalendar/daygrid': '5.11.5',
'@fullcalendar/interaction': '5.11.5',
'@fullcalendar/list': '5.11.5',
'@fullcalendar/timegrid': '5.11.5',
});
Package.onUse(function(api) {
api.use([
'momentjs:moment',
'templating'
], 'client');
api.addFiles([
'template.html',
'template.js',
'fullcalendar/fullcalendar.js',
'fullcalendar/fullcalendar.css',
'fullcalendar/locale-all.js',
'fullcalendar/gcal.js',
], 'client');
api.versionsFrom(['2.16', '3.0']);
api.use(['ecmascript', 'templating', 'tracker'], 'client');
api.addFiles(
[
'.npm/package/node_modules/@fullcalendar/common/main.min.css',
'.npm/package/node_modules/@fullcalendar/daygrid/main.min.css',
'.npm/package/node_modules/@fullcalendar/timegrid/main.min.css',
'.npm/package/node_modules/@fullcalendar/list/main.min.css',
'template.html',
'template.js',
],
'client',
);
});

View file

@ -1,11 +1,86 @@
window.moment = moment;
import { Template } from 'meteor/templating';
Template.fullcalendar.rendered = function() {
var div = this.$(this.firstNode);
if(this.data != null) {
//jquery takes care of undefined values, no need to check here
div.attr('id', this.data.id);
div.addClass(this.data.class);
const FullCalendarCore = require('@fullcalendar/core/main.cjs.js');
const FullCalendarDayGrid = require('@fullcalendar/daygrid/main.cjs.js');
const FullCalendarInteraction = require('@fullcalendar/interaction/main.cjs.js');
const FullCalendarList = require('@fullcalendar/list/main.cjs.js');
const FullCalendarTimeGrid = require('@fullcalendar/timegrid/main.cjs.js');
const FullCalendarLocalesAll = require('@fullcalendar/core/locales-all.js');
Template.fullcalendar.onRendered(function () {
const instance = this;
const container = this.find('div');
this.autorunHandle = this.autorun(() => {
const data = Template.currentData() || {};
let preservedViewType = null;
let preservedDate = null;
if (!container) {
return;
}
div.fullCalendar(this.data);
};
container.id = data.id || '';
container.className = data.class || '';
const options = { ...data };
delete options.id;
delete options.class;
if (options.defaultView && !options.initialView) {
options.initialView = options.defaultView;
}
delete options.defaultView;
if (options.header && !options.headerToolbar) {
options.headerToolbar = options.header;
}
delete options.header;
if (!options.locales && FullCalendarLocalesAll && FullCalendarLocalesAll.default) {
options.locales = FullCalendarLocalesAll.default;
}
if (instance.calendar) {
// Keep the user's current view/date when reactive data updates.
if (instance.calendar.view && instance.calendar.view.type) {
preservedViewType = instance.calendar.view.type;
}
if (instance.calendar.getDate) {
preservedDate = instance.calendar.getDate();
}
instance.calendar.destroy();
instance.calendar = null;
}
if (preservedViewType && !options.initialView) {
options.initialView = preservedViewType;
}
if (preservedDate && !options.initialDate) {
options.initialDate = preservedDate;
}
instance.calendar = new FullCalendarCore.Calendar(container, {
plugins: [
FullCalendarDayGrid.default,
FullCalendarInteraction.default,
FullCalendarList.default,
FullCalendarTimeGrid.default,
],
...options,
});
// Allow callers to manually access and refetch without jQuery plugin API.
container._wekanCalendar = instance.calendar;
instance.calendar.render();
});
});
Template.fullcalendar.onDestroyed(function () {
if (this.autorunHandle) {
this.autorunHandle.stop();
this.autorunHandle = null;
}
if (this.calendar) {
this.calendar.destroy();
this.calendar = null;
}
});