From 1b74fd8d4e00589ad840f3949b61db15f47a5559 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Tue, 24 Feb 2026 23:07:05 +0200 Subject: [PATCH 1/6] Migrate wekan-fullcalendar to npm FullCalendar v5 and remove Meteor moment package --- .eslintrc.json | 3 +- .meteor/packages | 1 - .meteor/versions | 3 +- client/components/activities/activities.jade | 6 +- client/components/activities/comments.jade | 2 +- client/components/boards/boardArchive.jade | 2 +- client/components/boards/boardBody.js | 89 ++++---- client/components/cards/cardDetails.jade | 2 +- client/components/cards/checklists.jade | 2 +- client/components/cards/subtasks.jade | 2 +- client/components/lists/listHeader.jade | 6 +- client/components/main/myCards.jade | 2 +- .../components/notifications/notification.js | 13 +- client/components/settings/peopleBody.jade | 12 +- client/components/settings/settingBody.js | 190 +++++++++++------- .../components/sidebar/sidebarArchives.jade | 6 +- client/config/blazeHelpers.js | 54 +++-- imports/lib/dateUtils.js | 99 ++++++--- package-lock.json | 20 +- packages/wekan-fullcalendar/package.js | 41 ++-- packages/wekan-fullcalendar/template.js | 93 ++++++++- 21 files changed, 415 insertions(+), 233 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 6d0addb49..013b76d80 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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, diff --git a/.meteor/packages b/.meteor/packages index e9a6af949..3211eb507 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -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 diff --git a/.meteor/versions b/.meteor/versions index 9ae1e21f4..8fa670345 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -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 diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade index 140335178..346b4a4fb 100644 --- a/client/components/activities/activities.jade +++ b/client/components/activities/activities.jade @@ -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 }} diff --git a/client/components/activities/comments.jade b/client/components/activities/comments.jade index 1860eb4f4..cbf497a67 100644 --- a/client/components/activities/comments.jade +++ b/client/components/activities/comments.jade @@ -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 diff --git a/client/components/boards/boardArchive.jade b/client/components/boards/boardArchive.jade index 839f183e1..76f3be0a5 100644 --- a/client/components/boards/boardArchive.jade +++ b/client/components/boards/boardArchive.jade @@ -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'}} diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 1f8919552..499a2167f 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -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,30 +904,35 @@ 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() { 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: { + 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: '', }, buttonText: { @@ -939,12 +950,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 +981,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 +1000,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'); @@ -1056,7 +1067,7 @@ BlazeComponent.extendComponent({ currentBoard._id, firstList._id, myTitle, - startDate.toDate(), + selectionInfo.start, firstSwimlane._id, function (error, result) { if (error) { diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade index f3be79410..277503b29 100644 --- a/client/components/cards/cardDetails.jade +++ b/client/components/cards/cardDetails.jade @@ -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'}} diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade index 0cd70f880..c2b60837d 100644 --- a/client/components/cards/checklists.jade +++ b/client/components/cards/checklists.jade @@ -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 diff --git a/client/components/cards/subtasks.jade b/client/components/cards/subtasks.jade index 3ea5ec5e3..f8e711c28 100644 --- a/client/components/cards/subtasks.jade +++ b/client/components/cards/subtasks.jade @@ -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"}}... diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade index 9434ae1eb..967209374 100644 --- a/client/components/lists/listHeader.jade +++ b/client/components/lists/listHeader.jade @@ -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 diff --git a/client/components/main/myCards.jade b/client/components/main/myCards.jade index e2e4ffd73..7d068c43f 100644 --- a/client/components/main/myCards.jade +++ b/client/components/main/myCards.jade @@ -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 diff --git a/client/components/notifications/notification.js b/client/components/notifications/notification.js index 77cc9fa4b..27a7ff793 100644 --- a/client/components/notifications/notification.js +++ b/client/components/notifications/notification.js @@ -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(); }, }); diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade index 0234d6074..a44f07c55 100644 --- a/client/components/settings/peopleBody.jade +++ b/client/components/settings/peopleBody.jade @@ -190,9 +190,9 @@ template(name="orgRow") else td {{ orgData.orgWebsite }} if orgData.orgIsActive - td {{ moment orgData.createdAt 'LLL' }} + td {{ displayDate orgData.createdAt 'LLL' }} else - td {{ moment orgData.createdAt 'LLL' }} + td {{ displayDate orgData.createdAt 'LLL' }} td if orgData.orgIsActive | {{_ 'yes'}} @@ -224,9 +224,9 @@ template(name="teamRow") else td {{ teamData.teamWebsite }} if teamData.teamIsActive - td {{ moment teamData.createdAt 'LLL' }} + td {{ displayDate teamData.createdAt 'LLL' }} else - td {{ moment teamData.createdAt 'LLL' }} + td {{ displayDate teamData.createdAt 'LLL' }} 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 {{ moment userData.createdAt 'LLL' }} + td {{ displayDate userData.createdAt 'LLL' }} else - td {{ moment userData.createdAt 'LLL' }} + td {{ displayDate userData.createdAt 'LLL' }} if userData.loginDisabled td input.selectUserChkBox(type="checkbox", disabled="disabled", id="{{userData._id}}") diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js index 29c6976e1..8736e823c 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -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; }, }); - diff --git a/client/components/sidebar/sidebarArchives.jade b/client/components/sidebar/sidebarArchives.jade index 0cad38dac..287533e09 100644 --- a/client/components/sidebar/sidebarArchives.jade +++ b/client/components/sidebar/sidebarArchives.jade @@ -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 diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js index beef694fa..6d74b658e 100644 --- a/client/config/blazeHelpers.js +++ b/client/config/blazeHelpers.js @@ -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); diff --git a/imports/lib/dateUtils.js b/imports/lib/dateUtils.js index a36ee469d..46df69382 100644 --- a/imports/lib/dateUtils.js +++ b/imports/lib/dateUtils.js @@ -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`; } diff --git a/package-lock.json b/package-lock.json index eda9a5d7b..2eb1fb617 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/packages/wekan-fullcalendar/package.js b/packages/wekan-fullcalendar/package.js index 38e8d64fc..d0f922f2c 100644 --- a/packages/wekan-fullcalendar/package.js +++ b/packages/wekan-fullcalendar/package.js @@ -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', + ); }); diff --git a/packages/wekan-fullcalendar/template.js b/packages/wekan-fullcalendar/template.js index a41d21d3d..069aa748c 100644 --- a/packages/wekan-fullcalendar/template.js +++ b/packages/wekan-fullcalendar/template.js @@ -1,11 +1,86 @@ -window.moment = moment; +import { Template } from 'meteor/templating'; +import { Tracker } from 'meteor/tracker'; -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 container = this.find('div'); + + this.autorunHandle = Tracker.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 (this.calendar) { + // Keep the user's current view/date when reactive data updates. + if (this.calendar.view && this.calendar.view.type) { + preservedViewType = this.calendar.view.type; + } + if (this.calendar.getDate) { + preservedDate = this.calendar.getDate(); + } + this.calendar.destroy(); + this.calendar = null; + } + + if (preservedViewType && !options.initialView) { + options.initialView = preservedViewType; + } + if (preservedDate && !options.initialDate) { + options.initialDate = preservedDate; + } + + this.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 = this.calendar; + this.calendar.render(); + }); +}); + +Template.fullcalendar.onDestroyed(function () { + if (this.autorunHandle) { + this.autorunHandle.stop(); + this.autorunHandle = null; + } + if (this.calendar) { + this.calendar.destroy(); + this.calendar = null; + } +}); From d25da91e0b37f14c24e5ae4de5838ef9bf37d632 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Tue, 24 Feb 2026 23:07:07 +0200 Subject: [PATCH 2/6] Document FullCalendar v5 wrapper usage and non-jQuery refetch flow --- packages/wekan-fullcalendar/README.md | 86 +++++++++++++-------------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/packages/wekan-fullcalendar/README.md b/packages/wekan-fullcalendar/README.md index 1a81e4764..68e117624 100644 --- a/packages/wekan-fullcalendar/README.md +++ b/packages/wekan-fullcalendar/README.md @@ -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([]); + }, + }; + }, +}); +``` - +### 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(); +} +``` From 2fe490ec3d5f36d72f39fcd417c570b932c0fe60 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Tue, 24 Feb 2026 23:14:27 +0200 Subject: [PATCH 3/6] Record FullCalendar post-Meteor-3.0 upgrade guidance in migration guide --- METEOR3_MIGRATION.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/METEOR3_MIGRATION.md b/METEOR3_MIGRATION.md index f933abe4e..587f4f2c2 100644 --- a/METEOR3_MIGRATION.md +++ b/METEOR3_MIGRATION.md @@ -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. From 8dafc774d8fd92dbf620eeeb7473a346ffad2701 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Tue, 24 Feb 2026 23:43:53 +0200 Subject: [PATCH 4/6] Use template-bound autorun in wekan-fullcalendar to prevent Blaze current view errors --- packages/wekan-fullcalendar/template.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/wekan-fullcalendar/template.js b/packages/wekan-fullcalendar/template.js index 069aa748c..cc4b13b7d 100644 --- a/packages/wekan-fullcalendar/template.js +++ b/packages/wekan-fullcalendar/template.js @@ -1,5 +1,4 @@ import { Template } from 'meteor/templating'; -import { Tracker } from 'meteor/tracker'; const FullCalendarCore = require('@fullcalendar/core/main.cjs.js'); const FullCalendarDayGrid = require('@fullcalendar/daygrid/main.cjs.js'); @@ -9,9 +8,10 @@ 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 = Tracker.autorun(() => { + this.autorunHandle = this.autorun(() => { const data = Template.currentData() || {}; let preservedViewType = null; let preservedDate = null; @@ -39,16 +39,16 @@ Template.fullcalendar.onRendered(function () { options.locales = FullCalendarLocalesAll.default; } - if (this.calendar) { + if (instance.calendar) { // Keep the user's current view/date when reactive data updates. - if (this.calendar.view && this.calendar.view.type) { - preservedViewType = this.calendar.view.type; + if (instance.calendar.view && instance.calendar.view.type) { + preservedViewType = instance.calendar.view.type; } - if (this.calendar.getDate) { - preservedDate = this.calendar.getDate(); + if (instance.calendar.getDate) { + preservedDate = instance.calendar.getDate(); } - this.calendar.destroy(); - this.calendar = null; + instance.calendar.destroy(); + instance.calendar = null; } if (preservedViewType && !options.initialView) { @@ -58,7 +58,7 @@ Template.fullcalendar.onRendered(function () { options.initialDate = preservedDate; } - this.calendar = new FullCalendarCore.Calendar(container, { + instance.calendar = new FullCalendarCore.Calendar(container, { plugins: [ FullCalendarDayGrid.default, FullCalendarInteraction.default, @@ -69,8 +69,8 @@ Template.fullcalendar.onRendered(function () { }); // Allow callers to manually access and refetch without jQuery plugin API. - container._wekanCalendar = this.calendar; - this.calendar.render(); + container._wekanCalendar = instance.calendar; + instance.calendar.render(); }); }); From 7b443b7bfc03641a3c64274b7c42f58af839d9f6 Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Tue, 24 Feb 2026 23:43:58 +0200 Subject: [PATCH 5/6] Refine calendar toolbar labels and style create-card modal controls consistently --- client/components/boards/boardBody.js | 25 ++++++++----- client/components/boards/calendarView.css | 43 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 client/components/boards/calendarView.css diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js index 499a2167f..57b993d93 100644 --- a/client/components/boards/boardBody.js +++ b/client/components/boards/boardBody.js @@ -911,6 +911,11 @@ BlazeComponent.extendComponent({ }); }, calendarOptions() { + const t = (key, fallback) => { + const translated = TAPi18n.__(key); + return translated && translated !== key ? translated : fallback; + }; + return { id: 'calendar-view', initialView: 'dayGridMonth', @@ -930,18 +935,20 @@ BlazeComponent.extendComponent({ meridiem: 'short', }, headerToolbar: { - left: 'title today prev,next', + left: 'title today prev,next', center: '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', @@ -1041,7 +1048,7 @@ BlazeComponent.extendComponent({ diff --git a/client/components/boards/calendarView.css b/client/components/boards/calendarView.css new file mode 100644 index 000000000..2b70a76cf --- /dev/null +++ b/client/components/boards/calendarView.css @@ -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; +} From b6f9eac37e85e9f55eba8bcb46b4130548be92ab Mon Sep 17 00:00:00 2001 From: Harry Adel Date: Tue, 24 Feb 2026 23:44:04 +0200 Subject: [PATCH 6/6] Add missing calendar translation keys for today day week and month --- imports/i18n/data/de_DE.i18n.json | 12 ++++++++---- imports/i18n/data/en.i18n.json | 12 ++++++++---- imports/i18n/data/es.i18n.json | 12 ++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/imports/i18n/data/de_DE.i18n.json b/imports/i18n/data/de_DE.i18n.json index 6a5701d3f..d0b0c8027 100644 --- a/imports/i18n/data/de_DE.i18n.json +++ b/imports/i18n/data/de_DE.i18n.json @@ -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": "/", diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index be4b09b82..d9ef3e7d3 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -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": "/", diff --git a/imports/i18n/data/es.i18n.json b/imports/i18n/data/es.i18n.json index 6581e7dac..dd7f6d4a0 100644 --- a/imports/i18n/data/es.i18n.json +++ b/imports/i18n/data/es.i18n.json @@ -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": "/",