mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 07:20:12 +01:00
Replaced moment.js with Javascript date.
Thanks to xet7 !
This commit is contained in:
parent
8c5b43295d
commit
cb6afe67a7
18 changed files with 933 additions and 222 deletions
|
|
@ -1,6 +1,25 @@
|
|||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Cards from '/models/cards';
|
||||
import { CustomFieldStringTemplate } from '/client/lib/customFields'
|
||||
|
||||
|
|
@ -134,18 +153,18 @@ CardCustomField.register('cardCustomField');
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(moment());
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
self.date.set(new Date(self.data().value));
|
||||
});
|
||||
}
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
return getISOWeek(this.date.get()).toString();
|
||||
}
|
||||
|
||||
showWeekOfYear() {
|
||||
|
|
@ -153,12 +172,7 @@ CardCustomField.register('cardCustomField');
|
|||
}
|
||||
|
||||
showDate() {
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
return calendar(this.date.get());
|
||||
}
|
||||
|
||||
showISODate() {
|
||||
|
|
@ -167,8 +181,8 @@ CardCustomField.register('cardCustomField');
|
|||
|
||||
classes() {
|
||||
if (
|
||||
this.date.get().isBefore(this.now.get(), 'minute') &&
|
||||
this.now.get().isBefore(this.data().value)
|
||||
isBefore(this.date.get(), this.now.get(), 'minute') &&
|
||||
isBefore(this.now.get(), this.data().value, 'minute')
|
||||
) {
|
||||
return 'current';
|
||||
}
|
||||
|
|
@ -176,7 +190,7 @@ CardCustomField.register('cardCustomField');
|
|||
}
|
||||
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().format('LLLL')}`;
|
||||
return `${TAPi18n.__('card-start-on')} ${this.date.get().toLocaleString()}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
|
|
@ -195,7 +209,7 @@ CardCustomField.register('cardCustomField');
|
|||
const self = this;
|
||||
self.card = Utils.getCurrentCard();
|
||||
self.customFieldId = this.data()._id;
|
||||
this.data().value && this.date.set(moment(this.data().value));
|
||||
this.data().value && this.date.set(new Date(this.data().value));
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,36 @@
|
|||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
// editCardReceivedDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getReceived() &&
|
||||
this.date.set(moment(this.data().getReceived()));
|
||||
this.date.set(new Date(this.data().getReceived()));
|
||||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setReceived(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
this.card.setReceived(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
|
|
@ -22,8 +41,8 @@ import { DatePicker } from '/client/lib/datepicker';
|
|||
// editCardStartDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getStart() && this.date.set(moment(this.data().getStart()));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getStart() && this.date.set(new Date(this.data().getStart()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
|
|
@ -37,7 +56,7 @@ import { DatePicker } from '/client/lib/datepicker';
|
|||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setStart(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
this.card.setStart(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
|
|
@ -49,7 +68,7 @@ import { DatePicker } from '/client/lib/datepicker';
|
|||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated('1970-01-01 17:00:00');
|
||||
this.data().getDue() && this.date.set(moment(this.data().getDue()));
|
||||
this.data().getDue() && this.date.set(new Date(this.data().getDue()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
|
|
@ -60,7 +79,7 @@ import { DatePicker } from '/client/lib/datepicker';
|
|||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setDue(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
this.card.setDue(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
|
|
@ -71,8 +90,8 @@ import { DatePicker } from '/client/lib/datepicker';
|
|||
// editCardEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getEnd() && this.date.set(moment(this.data().getEnd()));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getEnd() && this.date.set(new Date(this.data().getEnd()));
|
||||
}
|
||||
|
||||
onRendered() {
|
||||
|
|
@ -83,7 +102,7 @@ import { DatePicker } from '/client/lib/datepicker';
|
|||
}
|
||||
|
||||
_storeDate(date) {
|
||||
this.card.setEnd(moment(date).format('YYYY-MM-DD HH:mm'));
|
||||
this.card.setEnd(formatDateTime(date));
|
||||
}
|
||||
|
||||
_deleteDate() {
|
||||
|
|
@ -100,14 +119,14 @@ const CardDate = BlazeComponent.extendComponent({
|
|||
onCreated() {
|
||||
const self = this;
|
||||
self.date = ReactiveVar();
|
||||
self.now = ReactiveVar(moment());
|
||||
self.now = ReactiveVar(now());
|
||||
window.setInterval(() => {
|
||||
self.now.set(moment());
|
||||
self.now.set(now());
|
||||
}, 60000);
|
||||
},
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
return getISOWeek(this.date.get()).toString();
|
||||
},
|
||||
|
||||
showWeekOfYear() {
|
||||
|
|
@ -115,12 +134,7 @@ const CardDate = BlazeComponent.extendComponent({
|
|||
},
|
||||
|
||||
showDate() {
|
||||
// this will start working once mquandalle:moment
|
||||
// is updated to at least moment.js 2.10.5
|
||||
// until then, the date is displayed in the "L" format
|
||||
return this.date.get().calendar(null, {
|
||||
sameElse: 'llll',
|
||||
});
|
||||
return calendar(this.date.get());
|
||||
},
|
||||
|
||||
showISODate() {
|
||||
|
|
@ -133,7 +147,7 @@ class CardReceivedDate extends CardDate {
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getReceived()));
|
||||
self.date.set(new Date(self.data().getReceived()));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +187,7 @@ class CardStartDate extends CardDate {
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getStart()));
|
||||
self.date.set(new Date(self.data().getStart()));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +222,7 @@ class CardDueDate extends CardDate {
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getDue()));
|
||||
self.date.set(new Date(self.data().getDue()));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -244,7 +258,7 @@ class CardEndDate extends CardDate {
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getEnd()));
|
||||
self.date.set(new Date(self.data().getEnd()));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -279,12 +293,12 @@ class CardCustomFieldDate extends CardDate {
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().value));
|
||||
self.date.set(new Date(self.data().value));
|
||||
});
|
||||
}
|
||||
|
||||
showWeek() {
|
||||
return this.date.get().week().toString();
|
||||
return getISOWeek(this.date.get()).toString();
|
||||
}
|
||||
|
||||
showWeekOfYear() {
|
||||
|
|
@ -316,31 +330,31 @@ CardCustomFieldDate.register('cardCustomFieldDate');
|
|||
|
||||
(class extends CardReceivedDate {
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
return format(this.date.get(), 'L');
|
||||
}
|
||||
}.register('minicardReceivedDate'));
|
||||
|
||||
(class extends CardStartDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardStartDate'));
|
||||
|
||||
(class extends CardDueDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardDueDate'));
|
||||
|
||||
(class extends CardEndDate {
|
||||
showDate() {
|
||||
return this.date.get().format('YYYY-MM-DD HH:mm');
|
||||
return format(this.date.get(), 'YYYY-MM-DD HH:mm');
|
||||
}
|
||||
}.register('minicardEndDate'));
|
||||
|
||||
(class extends CardCustomFieldDate {
|
||||
showDate() {
|
||||
return this.date.get().format('L');
|
||||
return format(this.date.get(), 'L');
|
||||
}
|
||||
}.register('minicardCustomFieldDate'));
|
||||
|
||||
|
|
@ -349,7 +363,7 @@ class VoteEndDate extends CardDate {
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getVoteEnd()));
|
||||
self.date.set(new Date(self.data().getVoteEnd()));
|
||||
});
|
||||
}
|
||||
classes() {
|
||||
|
|
@ -357,10 +371,10 @@ class VoteEndDate extends CardDate {
|
|||
return classes;
|
||||
}
|
||||
showDate() {
|
||||
return this.date.get().format('L LT');
|
||||
return format(this.date.get(), 'L') + ' ' + format(this.date.get(), 'HH:mm');
|
||||
}
|
||||
showTitle() {
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().format('LLLL')}`;
|
||||
return `${TAPi18n.__('card-end-on')} ${this.date.get().toLocaleString()}`;
|
||||
}
|
||||
|
||||
events() {
|
||||
|
|
@ -376,7 +390,7 @@ class PokerEndDate extends CardDate {
|
|||
super.onCreated();
|
||||
const self = this;
|
||||
self.autorun(() => {
|
||||
self.date.set(moment(self.data().getPokerEnd()));
|
||||
self.date.set(new Date(self.data().getPokerEnd()));
|
||||
});
|
||||
}
|
||||
classes() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,26 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { DatePicker } from '/client/lib/datepicker';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Cards from '/models/cards';
|
||||
import Boards from '/models/boards';
|
||||
import Checklists from '/models/checklists';
|
||||
|
|
@ -455,7 +474,7 @@ BlazeComponent.extendComponent({
|
|||
'click .js-poker-finish'(e) {
|
||||
if ($(e.target).hasClass('js-poker-finish')) {
|
||||
e.preventDefault();
|
||||
const now = moment().format('YYYY-MM-DD HH:mm');
|
||||
const now = formatDateTime(new Date());
|
||||
this.data().setPokerEnd(now);
|
||||
}
|
||||
},
|
||||
|
|
@ -1106,8 +1125,8 @@ BlazeComponent.extendComponent({
|
|||
// editVoteEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
this.data().getVoteEnd() && this.date.set(moment(this.data().getVoteEnd()));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getVoteEnd() && this.date.set(new Date(this.data().getVoteEnd()));
|
||||
}
|
||||
events() {
|
||||
return [
|
||||
|
|
@ -1118,12 +1137,12 @@ BlazeComponent.extendComponent({
|
|||
// if no time was given, init with 12:00
|
||||
const time =
|
||||
evt.target.time.value ||
|
||||
moment(new Date().setHours(12, 0, 0)).format('LT');
|
||||
formatTime(new Date().setHours(12, 0, 0));
|
||||
|
||||
const dateString = `${evt.target.date.value} ${time}`;
|
||||
|
||||
/*
|
||||
const newDate = moment(dateString, 'L LT', true);
|
||||
const newDate = parseDate(dateString, ['L LT'], true);
|
||||
if (newDate.isValid()) {
|
||||
// if active vote - store it
|
||||
if (this.currentData().getVoteQuestion()) {
|
||||
|
|
@ -1137,28 +1156,27 @@ BlazeComponent.extendComponent({
|
|||
|
||||
*/
|
||||
|
||||
// Try to parse different date formats of all languages.
|
||||
// This code is same for vote and planning poker.
|
||||
const usaDate = moment(dateString, 'L LT', true);
|
||||
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
|
||||
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
|
||||
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
|
||||
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
|
||||
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
|
||||
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
|
||||
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
|
||||
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
|
||||
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
|
||||
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
|
||||
// greekDate does not work: el Greek Ελληνικά ,
|
||||
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
|
||||
// where MM is maybe some text like AM/PM ?
|
||||
// Also some other languages that have non-ascii characters in dates
|
||||
// do not work.
|
||||
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
|
||||
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
|
||||
// Try to parse different date formats using native Date parsing
|
||||
const formats = [
|
||||
'YYYY-MM-DD HH:mm',
|
||||
'MM/DD/YYYY HH:mm',
|
||||
'DD.MM.YYYY HH:mm',
|
||||
'DD/MM/YYYY HH:mm',
|
||||
'DD-MM-YYYY HH:mm'
|
||||
];
|
||||
|
||||
let parsedDate = null;
|
||||
for (const format of formats) {
|
||||
parsedDate = parseDate(dateString, [format], true);
|
||||
if (parsedDate) break;
|
||||
}
|
||||
|
||||
// Fallback to native Date parsing
|
||||
if (!parsedDate) {
|
||||
parsedDate = new Date(dateString);
|
||||
}
|
||||
|
||||
if (usaDate.isValid()) {
|
||||
if (isValidDate(parsedDate)) {
|
||||
// if active poker - store it
|
||||
if (this.currentData().getPokerQuestion()) {
|
||||
this._storeDate(usaDate.toDate());
|
||||
|
|
@ -1337,9 +1355,9 @@ BlazeComponent.extendComponent({
|
|||
// editPokerEndDatePopup
|
||||
(class extends DatePicker {
|
||||
onCreated() {
|
||||
super.onCreated(moment().format('YYYY-MM-DD HH:mm'));
|
||||
super.onCreated(formatDateTime(now()));
|
||||
this.data().getPokerEnd() &&
|
||||
this.date.set(moment(this.data().getPokerEnd()));
|
||||
this.date.set(new Date(this.data().getPokerEnd()));
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -1357,7 +1375,7 @@ BlazeComponent.extendComponent({
|
|||
return moment.localeData().longDateFormat('LT');
|
||||
}
|
||||
|
||||
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
|
||||
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
|
||||
*/
|
||||
|
||||
events() {
|
||||
|
|
@ -1369,7 +1387,7 @@ BlazeComponent.extendComponent({
|
|||
// if no time was given, init with 12:00
|
||||
const time =
|
||||
evt.target.time.value ||
|
||||
moment(new Date().setHours(12, 0, 0)).format('LT');
|
||||
formatTime(new Date().setHours(12, 0, 0));
|
||||
|
||||
const dateString = `${evt.target.date.value} ${time}`;
|
||||
|
||||
|
|
@ -1380,7 +1398,7 @@ BlazeComponent.extendComponent({
|
|||
Maybe client/components/lib/datepicker.jade could have hidden input field for
|
||||
datepicker format that could be used to detect date format?
|
||||
|
||||
const newDate = moment(dateString, dateformat() + ' ' + timeformat(), true);
|
||||
const newDate = parseDate(dateString, [dateformat() + ' ' + timeformat()], true);
|
||||
|
||||
if (newDate.isValid()) {
|
||||
// if active poker - store it
|
||||
|
|
@ -1393,28 +1411,27 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
*/
|
||||
|
||||
// Try to parse different date formats of all languages.
|
||||
// This code is same for vote and planning poker.
|
||||
const usaDate = moment(dateString, 'L LT', true);
|
||||
const euroAmDate = moment(dateString, 'DD.MM.YYYY LT', true);
|
||||
const euro24hDate = moment(dateString, 'DD.MM.YYYY HH.mm', true);
|
||||
const eurodotDate = moment(dateString, 'DD.MM.YYYY HH:mm', true);
|
||||
const minusDate = moment(dateString, 'YYYY-MM-DD HH:mm', true);
|
||||
const slashDate = moment(dateString, 'DD/MM/YYYY HH.mm', true);
|
||||
const dotDate = moment(dateString, 'DD/MM/YYYY HH:mm', true);
|
||||
const brezhonegDate = moment(dateString, 'DD/MM/YYYY h[e]mm A', true);
|
||||
const hrvatskiDate = moment(dateString, 'DD. MM. YYYY H:mm', true);
|
||||
const latviaDate = moment(dateString, 'YYYY.MM.DD. H:mm', true);
|
||||
const nederlandsDate = moment(dateString, 'DD-MM-YYYY HH:mm', true);
|
||||
// greekDate does not work: el Greek Ελληνικά ,
|
||||
// it has date format DD/MM/YYYY h:mm MM like 20/06/2021 11:15 MM
|
||||
// where MM is maybe some text like AM/PM ?
|
||||
// Also some other languages that have non-ascii characters in dates
|
||||
// do not work.
|
||||
const greekDate = moment(dateString, 'DD/MM/YYYY h:mm A', true);
|
||||
const macedonianDate = moment(dateString, 'D.MM.YYYY H:mm', true);
|
||||
// Try to parse different date formats using native Date parsing
|
||||
const formats = [
|
||||
'YYYY-MM-DD HH:mm',
|
||||
'MM/DD/YYYY HH:mm',
|
||||
'DD.MM.YYYY HH:mm',
|
||||
'DD/MM/YYYY HH:mm',
|
||||
'DD-MM-YYYY HH:mm'
|
||||
];
|
||||
|
||||
let parsedDate = null;
|
||||
for (const format of formats) {
|
||||
parsedDate = parseDate(dateString, [format], true);
|
||||
if (parsedDate) break;
|
||||
}
|
||||
|
||||
// Fallback to native Date parsing
|
||||
if (!parsedDate) {
|
||||
parsedDate = new Date(dateString);
|
||||
}
|
||||
|
||||
if (usaDate.isValid()) {
|
||||
if (isValidDate(parsedDate)) {
|
||||
// if active poker - store it
|
||||
if (this.currentData().getPokerQuestion()) {
|
||||
this._storeDate(usaDate.toDate());
|
||||
|
|
|
|||
|
|
@ -1,7 +1,26 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { Session } from 'meteor/session';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
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', () => {
|
||||
const ret = Utils.getCurrentBoard();
|
||||
|
|
@ -47,7 +66,7 @@ Blaze.registerHelper('isTouchScreenOrShowDesktopDragHandles', () =>
|
|||
Blaze.registerHelper('moment', (...args) => {
|
||||
args.pop(); // hash
|
||||
const [date, format] = args;
|
||||
return moment(date).format(format ?? 'LLLL');
|
||||
return format(new Date(date), format ?? 'LLLL');
|
||||
});
|
||||
|
||||
Blaze.registerHelper('canModifyCard', () =>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,29 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
// Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours
|
||||
// Helper function to get time format for 24 hours
|
||||
function adjustedTimeFormat() {
|
||||
return moment
|
||||
.localeData()
|
||||
.longDateFormat('LT');
|
||||
return 'HH:mm';
|
||||
}
|
||||
|
||||
// .replace(/HH/i, 'H');
|
||||
|
|
@ -19,7 +36,7 @@ export class DatePicker extends BlazeComponent {
|
|||
onCreated(defaultTime = '1970-01-01 08:00:00') {
|
||||
this.error = new ReactiveVar('');
|
||||
this.card = this.data();
|
||||
this.date = new ReactiveVar(moment.invalid());
|
||||
this.date = new ReactiveVar(new Date('invalid'));
|
||||
this.defaultTime = defaultTime;
|
||||
}
|
||||
|
||||
|
|
@ -34,35 +51,35 @@ export class DatePicker extends BlazeComponent {
|
|||
|
||||
onRendered() {
|
||||
// Set initial values for native HTML inputs
|
||||
if (this.date.get().isValid()) {
|
||||
if (isValidDate(this.date.get())) {
|
||||
const dateInput = this.find('#date');
|
||||
const timeInput = this.find('#time');
|
||||
|
||||
if (dateInput) {
|
||||
dateInput.value = this.date.get().format('YYYY-MM-DD');
|
||||
dateInput.value = formatDate(this.date.get());
|
||||
}
|
||||
if (timeInput && !timeInput.value && this.defaultTime) {
|
||||
const defaultMoment = moment(this.defaultTime);
|
||||
timeInput.value = defaultMoment.format('HH:mm');
|
||||
} else if (timeInput && this.date.get().isValid()) {
|
||||
timeInput.value = this.date.get().format('HH:mm');
|
||||
const defaultDate = new Date(this.defaultTime);
|
||||
timeInput.value = formatTime(defaultDate);
|
||||
} else if (timeInput && isValidDate(this.date.get())) {
|
||||
timeInput.value = formatTime(this.date.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showDate() {
|
||||
if (this.date.get().isValid()) return this.date.get().format('YYYY-MM-DD');
|
||||
if (isValidDate(this.date.get())) return formatDate(this.date.get());
|
||||
return '';
|
||||
}
|
||||
showTime() {
|
||||
if (this.date.get().isValid()) return this.date.get().format('HH:mm');
|
||||
if (isValidDate(this.date.get())) return formatTime(this.date.get());
|
||||
return '';
|
||||
}
|
||||
dateFormat() {
|
||||
return moment.localeData().longDateFormat('L');
|
||||
return 'L';
|
||||
}
|
||||
timeFormat() {
|
||||
return moment.localeData().longDateFormat('LT');
|
||||
return 'LT';
|
||||
}
|
||||
|
||||
events() {
|
||||
|
|
@ -72,8 +89,8 @@ export class DatePicker extends BlazeComponent {
|
|||
// Native HTML date input validation
|
||||
const dateValue = this.find('#date').value;
|
||||
if (dateValue) {
|
||||
const dateMoment = moment(dateValue, 'YYYY-MM-DD', true);
|
||||
if (dateMoment.isValid()) {
|
||||
const dateObj = new Date(dateValue);
|
||||
if (isValidDate(dateObj)) {
|
||||
this.error.set('');
|
||||
} else {
|
||||
this.error.set('invalid-date');
|
||||
|
|
@ -84,8 +101,8 @@ export class DatePicker extends BlazeComponent {
|
|||
// Native HTML time input validation
|
||||
const timeValue = this.find('#time').value;
|
||||
if (timeValue) {
|
||||
const timeMoment = moment(timeValue, 'HH:mm', true);
|
||||
if (timeMoment.isValid()) {
|
||||
const timeObj = new Date(`1970-01-01T${timeValue}`);
|
||||
if (isValidDate(timeObj)) {
|
||||
this.error.set('');
|
||||
} else {
|
||||
this.error.set('invalid-time');
|
||||
|
|
@ -104,14 +121,14 @@ export class DatePicker extends BlazeComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
const newCompleteDate = moment(`${dateValue} ${timeValue}`, 'YYYY-MM-DD HH:mm', true);
|
||||
const newCompleteDate = new Date(`${dateValue}T${timeValue}`);
|
||||
|
||||
if (!newCompleteDate.isValid()) {
|
||||
if (!isValidDate(newCompleteDate)) {
|
||||
this.error.set('invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
this._storeDate(newCompleteDate.toDate());
|
||||
this._storeDate(newCompleteDate);
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-delete-date'(evt) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,24 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
// Filtered view manager
|
||||
// We define local filter objects for each different type of field (SetFilter,
|
||||
|
|
@ -30,7 +49,7 @@ class DateFilter {
|
|||
this.reset();
|
||||
return;
|
||||
}
|
||||
this._filter = { $lte: moment().toDate() };
|
||||
this._filter = { $lte: now() };
|
||||
this._updateState('past');
|
||||
}
|
||||
|
||||
|
|
@ -72,13 +91,8 @@ class DateFilter {
|
|||
return;
|
||||
}
|
||||
|
||||
var startDay = moment()
|
||||
.startOf('day')
|
||||
.toDate(),
|
||||
endDay = moment()
|
||||
.endOf('day')
|
||||
.add(offset, 'day')
|
||||
.toDate();
|
||||
var startDay = startOf(now(), 'day'),
|
||||
endDay = endOf(add(now(), offset, 'day'), 'day');
|
||||
|
||||
if (offset >= 0) {
|
||||
this._filter = { $gte: startDay, $lte: endDay };
|
||||
|
|
@ -112,33 +126,21 @@ class DateFilter {
|
|||
const weekStartDay = currentUser ? currentUser.getStartDayOfWeek() : 1;
|
||||
|
||||
if (week === 'this') {
|
||||
// Moments are mutable so they must be cloned before modification
|
||||
var WeekStart = moment()
|
||||
.startOf('day')
|
||||
.startOf('week')
|
||||
.add(weekStartDay, 'days');
|
||||
var WeekEnd = WeekStart
|
||||
.clone()
|
||||
.add(6, 'days')
|
||||
.endOf('day');
|
||||
// Create week start and end dates
|
||||
var WeekStart = startOf(add(startOf(now(), 'week'), weekStartDay, 'days'), 'day');
|
||||
var WeekEnd = endOf(add(WeekStart, 6, 'days'), 'day');
|
||||
|
||||
this._updateState('thisweek');
|
||||
} else if (week === 'next') {
|
||||
// Moments are mutable so they must be cloned before modification
|
||||
var WeekStart = moment()
|
||||
.startOf('day')
|
||||
.startOf('week')
|
||||
.add(weekStartDay + 7, 'days');
|
||||
var WeekEnd = WeekStart
|
||||
.clone()
|
||||
.add(6, 'days')
|
||||
.endOf('day');
|
||||
// Create next week start and end dates
|
||||
var WeekStart = startOf(add(startOf(now(), 'week'), weekStartDay + 7, 'days'), 'day');
|
||||
var WeekEnd = endOf(add(WeekStart, 6, 'days'), 'day');
|
||||
|
||||
this._updateState('nextweek');
|
||||
}
|
||||
|
||||
var startDate = WeekStart.toDate();
|
||||
var endDate = WeekEnd.toDate();
|
||||
var startDate = WeekStart;
|
||||
var endDate = WeekEnd;
|
||||
|
||||
if (offset >= 0) {
|
||||
this._filter = { $gte: startDate, $lte: endDate };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,24 @@
|
|||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import {
|
||||
OPERATOR_ASSIGNEE,
|
||||
OPERATOR_BOARD,
|
||||
|
|
@ -421,43 +440,44 @@ export class Query {
|
|||
switch (duration) {
|
||||
case PREDICATE_WEEK:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const week = moment().week();
|
||||
const week = getISOWeek(now());
|
||||
if (week === 52) {
|
||||
date = moment(1, 'W');
|
||||
date.set('year', date.year() + 1);
|
||||
date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
|
||||
} else {
|
||||
date = moment(week + 1, 'W');
|
||||
// Calculate the date for the next week
|
||||
const currentDate = now();
|
||||
const daysToAdd = (week + 1) * 7 - (currentDate.getDay() + 6) % 7;
|
||||
date = add(currentDate, daysToAdd, 'days');
|
||||
}
|
||||
break;
|
||||
case PREDICATE_MONTH:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const month = moment().month();
|
||||
// .month() is zero indexed
|
||||
const month = now().getMonth();
|
||||
// .getMonth() is zero indexed
|
||||
if (month === 11) {
|
||||
date = moment(1, 'M');
|
||||
date.set('year', date.year() + 1);
|
||||
date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
|
||||
} else {
|
||||
date = moment(month + 2, 'M');
|
||||
date = new Date(now().getFullYear(), month + 1, 1); // First day of next month
|
||||
}
|
||||
break;
|
||||
case PREDICATE_QUARTER:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const quarter = moment().quarter();
|
||||
const quarter = Math.floor(now().getMonth() / 3) + 1;
|
||||
if (quarter === 4) {
|
||||
date = moment(1, 'Q');
|
||||
date.set('year', date.year() + 1);
|
||||
date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
|
||||
} else {
|
||||
date = moment(quarter + 1, 'Q');
|
||||
const nextQuarterMonth = quarter * 3; // 3, 6, 9 for quarters 2, 3, 4
|
||||
date = new Date(now().getFullYear(), nextQuarterMonth, 1); // First day of next quarter
|
||||
}
|
||||
break;
|
||||
case PREDICATE_YEAR:
|
||||
date = moment(moment().year() + 1, 'YYYY');
|
||||
date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
|
||||
break;
|
||||
}
|
||||
if (date) {
|
||||
value = {
|
||||
operator: '$lt',
|
||||
value: date.format('YYYY-MM-DD'),
|
||||
value: formatDate(date),
|
||||
};
|
||||
}
|
||||
} else if (
|
||||
|
|
@ -466,7 +486,7 @@ export class Query {
|
|||
) {
|
||||
value = {
|
||||
operator: '$lt',
|
||||
value: moment().format('YYYY-MM-DD'),
|
||||
value: formatDate(now()),
|
||||
};
|
||||
} else {
|
||||
this.addError(OPERATOR_DUE, {
|
||||
|
|
@ -478,16 +498,12 @@ export class Query {
|
|||
} else if (operator === OPERATOR_DUE) {
|
||||
value = {
|
||||
operator: '$lt',
|
||||
value: moment(moment().format('YYYY-MM-DD'))
|
||||
.add(days + 1, duration ? duration : 'days')
|
||||
.format(),
|
||||
value: formatDate(add(add(now(), 1, 'days'), days + 1, duration ? duration : 'days')),
|
||||
};
|
||||
} else {
|
||||
value = {
|
||||
operator: '$gte',
|
||||
value: moment(moment().format('YYYY-MM-DD'))
|
||||
.subtract(days, duration ? duration : 'days')
|
||||
.format(),
|
||||
value: formatDate(subtract(now(), days, duration ? duration : 'days')),
|
||||
};
|
||||
}
|
||||
} else if (operator === OPERATOR_SORT) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { TAPi18n } from './tap';
|
||||
import './accounts';
|
||||
import './moment';
|
||||
|
||||
if (Meteor.isClient) {
|
||||
import './blaze';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
import { Tracker } from 'meteor/tracker';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from './tap';
|
||||
|
||||
// Reactively adjust Moment.js translations
|
||||
Tracker.autorun(() => {
|
||||
const language = TAPi18n.getLanguage();
|
||||
try {
|
||||
moment.locale(language);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
// Note: moment.js has been removed and replaced with native JavaScript Date functions
|
||||
// Locale handling is now done through native Date.toLocaleString() methods
|
||||
|
|
|
|||
492
imports/lib/dateUtils.js
Normal file
492
imports/lib/dateUtils.js
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
/**
|
||||
* Date utility functions to replace moment.js with native JavaScript Date
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a date to YYYY-MM-DD HH:mm format
|
||||
* @param {Date|string} date - Date to format
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
export function formatDateTime(date) {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date to YYYY-MM-DD format
|
||||
* @param {Date|string} date - Date to format
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
export function formatDate(date) {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a time to HH:mm format
|
||||
* @param {Date|string} date - Date to format
|
||||
* @returns {string} Formatted time string
|
||||
*/
|
||||
export function formatTime(date) {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO week number (ISO 8601)
|
||||
* @param {Date|string} date - Date to get week number for
|
||||
* @returns {number} ISO week number
|
||||
*/
|
||||
export function getISOWeek(date) {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return 0;
|
||||
|
||||
// Set to nearest Thursday: current date + 4 - current day number
|
||||
// Make Sunday's day number 7
|
||||
const target = new Date(d);
|
||||
const dayNr = (d.getDay() + 6) % 7;
|
||||
target.setDate(target.getDate() - dayNr + 3);
|
||||
|
||||
// ISO week date weeks start on monday, so correct the day number
|
||||
const firstThursday = target.valueOf();
|
||||
target.setMonth(0, 1);
|
||||
if (target.getDay() !== 4) {
|
||||
target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
|
||||
}
|
||||
|
||||
return 1 + Math.ceil((firstThursday - target) / 604800000); // 604800000 = 7 * 24 * 3600 * 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a date is valid
|
||||
* @param {Date|string} date - Date to check
|
||||
* @returns {boolean} True if date is valid
|
||||
*/
|
||||
export function isValidDate(date) {
|
||||
const d = new Date(date);
|
||||
return !isNaN(d.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a date is before another date
|
||||
* @param {Date|string} date1 - First date
|
||||
* @param {Date|string} date2 - Second date
|
||||
* @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
|
||||
* @returns {boolean} True if date1 is before date2
|
||||
*/
|
||||
export function isBefore(date1, date2, unit = 'millisecond') {
|
||||
const d1 = new Date(date1);
|
||||
const d2 = new Date(date2);
|
||||
|
||||
if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
|
||||
|
||||
switch (unit) {
|
||||
case 'year':
|
||||
return d1.getFullYear() < d2.getFullYear();
|
||||
case 'month':
|
||||
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());
|
||||
case 'hour':
|
||||
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));
|
||||
default:
|
||||
return d1.getTime() < d2.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a date is after another date
|
||||
* @param {Date|string} date1 - First date
|
||||
* @param {Date|string} date2 - Second date
|
||||
* @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
|
||||
* @returns {boolean} True if date1 is after date2
|
||||
*/
|
||||
export function isAfter(date1, date2, unit = 'millisecond') {
|
||||
return isBefore(date2, date1, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a date is the same as another date
|
||||
* @param {Date|string} date1 - First date
|
||||
* @param {Date|string} date2 - Second date
|
||||
* @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
|
||||
* @returns {boolean} True if dates are the same
|
||||
*/
|
||||
export function isSame(date1, date2, unit = 'millisecond') {
|
||||
const d1 = new Date(date1);
|
||||
const d2 = new Date(date2);
|
||||
|
||||
if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
|
||||
|
||||
switch (unit) {
|
||||
case 'year':
|
||||
return d1.getFullYear() === d2.getFullYear();
|
||||
case 'month':
|
||||
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
|
||||
case 'day':
|
||||
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));
|
||||
case 'minute':
|
||||
return Math.floor(d1.getTime() / (1000 * 60)) === Math.floor(d2.getTime() / (1000 * 60));
|
||||
default:
|
||||
return d1.getTime() === d2.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add time to a date
|
||||
* @param {Date|string} date - Base date
|
||||
* @param {number} amount - Amount to add
|
||||
* @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
|
||||
* @returns {Date} New date
|
||||
*/
|
||||
export function add(date, amount, unit) {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return new Date();
|
||||
|
||||
switch (unit) {
|
||||
case 'years':
|
||||
d.setFullYear(d.getFullYear() + amount);
|
||||
break;
|
||||
case 'months':
|
||||
d.setMonth(d.getMonth() + amount);
|
||||
break;
|
||||
case 'days':
|
||||
d.setDate(d.getDate() + amount);
|
||||
break;
|
||||
case 'hours':
|
||||
d.setHours(d.getHours() + amount);
|
||||
break;
|
||||
case 'minutes':
|
||||
d.setMinutes(d.getMinutes() + amount);
|
||||
break;
|
||||
case 'seconds':
|
||||
d.setSeconds(d.getSeconds() + amount);
|
||||
break;
|
||||
default:
|
||||
d.setTime(d.getTime() + amount);
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract time from a date
|
||||
* @param {Date|string} date - Base date
|
||||
* @param {number} amount - Amount to subtract
|
||||
* @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
|
||||
* @returns {Date} New date
|
||||
*/
|
||||
export function subtract(date, amount, unit) {
|
||||
return add(date, -amount, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get start of a time unit
|
||||
* @param {Date|string} date - Base date
|
||||
* @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
|
||||
* @returns {Date} Start of unit
|
||||
*/
|
||||
export function startOf(date, unit) {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return new Date();
|
||||
|
||||
switch (unit) {
|
||||
case 'year':
|
||||
d.setMonth(0, 1);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case 'month':
|
||||
d.setDate(1);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case 'day':
|
||||
d.setHours(0, 0, 0, 0);
|
||||
break;
|
||||
case 'hour':
|
||||
d.setMinutes(0, 0, 0);
|
||||
break;
|
||||
case 'minute':
|
||||
d.setSeconds(0, 0);
|
||||
break;
|
||||
case 'second':
|
||||
d.setMilliseconds(0);
|
||||
break;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end of a time unit
|
||||
* @param {Date|string} date - Base date
|
||||
* @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
|
||||
* @returns {Date} End of unit
|
||||
*/
|
||||
export function endOf(date, unit) {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return new Date();
|
||||
|
||||
switch (unit) {
|
||||
case 'year':
|
||||
d.setMonth(11, 31);
|
||||
d.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
case 'month':
|
||||
d.setMonth(d.getMonth() + 1, 0);
|
||||
d.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
case 'day':
|
||||
d.setHours(23, 59, 59, 999);
|
||||
break;
|
||||
case 'hour':
|
||||
d.setMinutes(59, 59, 999);
|
||||
break;
|
||||
case 'minute':
|
||||
d.setSeconds(59, 999);
|
||||
break;
|
||||
case 'second':
|
||||
d.setMilliseconds(999);
|
||||
break;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for display with locale
|
||||
* @param {Date|string} date - Date to format
|
||||
* @param {string} format - Format string (simplified)
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
export function format(date, format = 'L') {
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
switch (format) {
|
||||
case 'L':
|
||||
return `${month}/${day}/${year}`;
|
||||
case 'LL':
|
||||
return d.toLocaleDateString();
|
||||
case 'LLL':
|
||||
return d.toLocaleString();
|
||||
case 'llll':
|
||||
return d.toLocaleString();
|
||||
case 'YYYY-MM-DD':
|
||||
return `${year}-${month}-${day}`;
|
||||
case 'YYYY-MM-DD HH:mm':
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
case 'HH:mm':
|
||||
return `${hours}:${minutes}`;
|
||||
default:
|
||||
return d.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date string with multiple formats
|
||||
* @param {string} dateString - Date string to parse
|
||||
* @param {string[]} formats - Array of format strings to try
|
||||
* @param {boolean} strict - Whether to use strict parsing
|
||||
* @returns {Date|null} Parsed date or null if invalid
|
||||
*/
|
||||
export function parseDate(dateString, formats = [], strict = true) {
|
||||
if (!dateString) return null;
|
||||
|
||||
// Try native Date parsing first
|
||||
const nativeDate = new Date(dateString);
|
||||
if (!isNaN(nativeDate.getTime())) {
|
||||
return nativeDate;
|
||||
}
|
||||
|
||||
// Try common formats
|
||||
const commonFormats = [
|
||||
'YYYY-MM-DD HH:mm',
|
||||
'YYYY-MM-DD',
|
||||
'MM/DD/YYYY HH:mm',
|
||||
'MM/DD/YYYY',
|
||||
'DD.MM.YYYY HH:mm',
|
||||
'DD.MM.YYYY',
|
||||
'DD/MM/YYYY HH:mm',
|
||||
'DD/MM/YYYY',
|
||||
'DD-MM-YYYY HH:mm',
|
||||
'DD-MM-YYYY'
|
||||
];
|
||||
|
||||
const allFormats = [...formats, ...commonFormats];
|
||||
|
||||
for (const format of allFormats) {
|
||||
const parsed = parseWithFormat(dateString, format);
|
||||
if (parsed && isValidDate(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date with specific format
|
||||
* @param {string} dateString - Date string to parse
|
||||
* @param {string} format - Format string
|
||||
* @returns {Date|null} Parsed date or null
|
||||
*/
|
||||
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}'
|
||||
};
|
||||
|
||||
let regex = format;
|
||||
for (const [key, value] of Object.entries(formatMap)) {
|
||||
regex = regex.replace(new RegExp(key, 'g'), `(${value})`);
|
||||
}
|
||||
|
||||
const match = dateString.match(new RegExp(regex));
|
||||
if (!match) return null;
|
||||
|
||||
const groups = match.slice(1);
|
||||
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') {
|
||||
year = parseInt(groups[groupIndex++]);
|
||||
i += 3;
|
||||
} else if (format[i] === 'M' && format[i + 1] === 'M') {
|
||||
month = parseInt(groups[groupIndex++]) - 1;
|
||||
i += 1;
|
||||
} else if (format[i] === 'D' && format[i + 1] === 'D') {
|
||||
day = parseInt(groups[groupIndex++]);
|
||||
i += 1;
|
||||
} else if (format[i] === 'H' && format[i + 1] === 'H') {
|
||||
hour = parseInt(groups[groupIndex++]);
|
||||
i += 1;
|
||||
} else if (format[i] === 'm' && format[i + 1] === 'm') {
|
||||
minute = parseInt(groups[groupIndex++]);
|
||||
i += 1;
|
||||
} else if (format[i] === 's' && format[i + 1] === 's') {
|
||||
second = parseInt(groups[groupIndex++]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (year === undefined || month === undefined || day === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current date and time
|
||||
* @returns {Date} Current date
|
||||
*/
|
||||
export function now() {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a date from components
|
||||
* @param {number} year - Year
|
||||
* @param {number} month - Month (0-based)
|
||||
* @param {number} day - Day
|
||||
* @param {number} hour - Hour (optional)
|
||||
* @param {number} minute - Minute (optional)
|
||||
* @param {number} second - Second (optional)
|
||||
* @returns {Date} Created date
|
||||
*/
|
||||
export function createDate(year, month, day, hour = 0, minute = 0, second = 0) {
|
||||
return new Date(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative time string (e.g., "2 hours ago")
|
||||
* @param {Date|string} date - Date to compare
|
||||
* @param {Date|string} now - Current date (optional)
|
||||
* @returns {string} Relative time string
|
||||
*/
|
||||
export function fromNow(date, now = new Date()) {
|
||||
const d = new Date(date);
|
||||
const n = new Date(now);
|
||||
|
||||
if (isNaN(d.getTime()) || isNaN(n.getTime())) return '';
|
||||
|
||||
const diffMs = n.getTime() - d.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
const diffWeeks = Math.floor(diffDays / 7);
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
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 (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`;
|
||||
return `${diffYears} year${diffYears !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calendar format (e.g., "Today", "Yesterday", "Tomorrow")
|
||||
* @param {Date|string} date - Date to format
|
||||
* @param {Date|string} now - Current date (optional)
|
||||
* @returns {string} Calendar format string
|
||||
*/
|
||||
export function calendar(date, now = new Date()) {
|
||||
const d = new Date(date);
|
||||
const n = new Date(now);
|
||||
|
||||
if (isNaN(d.getTime()) || isNaN(n.getTime())) return format(d);
|
||||
|
||||
const diffMs = d.getTime() - n.getTime();
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) return 'Today';
|
||||
if (diffDays === 1) return 'Tomorrow';
|
||||
if (diffDays === -1) return 'Yesterday';
|
||||
if (diffDays > 1 && diffDays < 7) return `In ${diffDays} days`;
|
||||
if (diffDays < -1 && diffDays > -7) return `${Math.abs(diffDays)} days ago`;
|
||||
|
||||
return format(d, 'L');
|
||||
}
|
||||
|
|
@ -1,5 +1,24 @@
|
|||
import { ReactiveCache, ReactiveMiniMongoIndex } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import {
|
||||
ALLOWED_COLORS,
|
||||
TYPE_CARD,
|
||||
|
|
@ -1492,8 +1511,8 @@ Cards.helpers({
|
|||
expiredVote() {
|
||||
let end = this.getVoteEnd();
|
||||
if (end) {
|
||||
end = moment(end);
|
||||
return end.isBefore(new Date());
|
||||
end = new Date(end);
|
||||
return isBefore(end, new Date());
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
|
@ -1586,8 +1605,8 @@ Cards.helpers({
|
|||
expiredPoker() {
|
||||
let end = this.getPokerEnd();
|
||||
if (end) {
|
||||
end = moment(end);
|
||||
return end.isBefore(new Date());
|
||||
end = new Date(end);
|
||||
return isBefore(end, new Date());
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
|
@ -3201,9 +3220,7 @@ if (Meteor.isServer) {
|
|||
// change list modifiedAt, when user modified the key values in
|
||||
// timingaction array, if it's endAt, put the modifiedAt of list
|
||||
// back to one year ago for sorting purpose
|
||||
const modifiedAt = moment()
|
||||
.subtract(1, 'year')
|
||||
.toISOString();
|
||||
const modifiedAt = add(now(), -1, 'year').toISOString();
|
||||
const boardId = list.boardId;
|
||||
Lists.direct.update(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,26 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
const Papa = require('papaparse');
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
//const stringify = require('csv-stringify');
|
||||
|
||||
|
|
@ -302,15 +321,15 @@ export class Exporter {
|
|||
labels = `${labels + label.name}-${label.color} `;
|
||||
});
|
||||
currentRow.push(labels.trim());
|
||||
currentRow.push(card.startAt ? moment(card.startAt).format() : ' ');
|
||||
currentRow.push(card.dueAt ? moment(card.dueAt).format() : ' ');
|
||||
currentRow.push(card.endAt ? moment(card.endAt).format() : ' ');
|
||||
currentRow.push(card.startAt ? new Date(card.startAt).toISOString() : ' ');
|
||||
currentRow.push(card.dueAt ? new Date(card.dueAt).toISOString() : ' ');
|
||||
currentRow.push(card.endAt ? new Date(card.endAt).toISOString() : ' ');
|
||||
currentRow.push(card.isOvertime ? 'true' : 'false');
|
||||
currentRow.push(card.spentTime);
|
||||
currentRow.push(card.createdAt ? moment(card.createdAt).format() : ' ');
|
||||
currentRow.push(card.modifiedAt ? moment(card.modifiedAt).format() : ' ');
|
||||
currentRow.push(card.createdAt ? new Date(card.createdAt).toISOString() : ' ');
|
||||
currentRow.push(card.modifiedAt ? new Date(card.modifiedAt).toISOString() : ' ');
|
||||
currentRow.push(
|
||||
card.dateLastActivity ? moment(card.dateLastActivity).format() : ' ',
|
||||
card.dateLastActivity ? new Date(card.dateLastActivity).toISOString() : ' ',
|
||||
);
|
||||
if (card.vote && card.vote.question !== '') {
|
||||
let positiveVoters = '';
|
||||
|
|
@ -343,7 +362,7 @@ export class Exporter {
|
|||
if (field.value !== null) {
|
||||
if (customFieldMap[field._id].type === 'date') {
|
||||
customFieldValuesToPush[customFieldMap[field._id].position] =
|
||||
moment(field.value).format();
|
||||
new Date(field.value).toISOString();
|
||||
} else if (customFieldMap[field._id].type === 'dropdown') {
|
||||
const dropdownOptions = result.customFields.find(
|
||||
({ _id }) => _id === field._id,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,26 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||
import { createWorkbook } from './createWorkbook';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
class ExporterCardPDF {
|
||||
constructor(boardId) {
|
||||
|
|
@ -353,8 +373,8 @@ class ExporterCardPDF {
|
|||
//add data +8 hours
|
||||
function addTZhours(jdate) {
|
||||
const curdate = new Date(jdate);
|
||||
const checkCorrectDate = moment(curdate);
|
||||
if (checkCorrectDate.isValid()) {
|
||||
const checkCorrectDate = new Date(curdate);
|
||||
if (isValidDate(checkCorrectDate)) {
|
||||
return curdate;
|
||||
} else {
|
||||
return ' ';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,26 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import { createWorkbook } from './createWorkbook';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||
|
||||
|
|
@ -379,8 +398,8 @@ class ExporterExcel {
|
|||
function addTZhours(jdate) {
|
||||
if (!jdate) { return ' '; }
|
||||
const curdate = new Date(jdate);
|
||||
const checkCorrectDate = moment(curdate);
|
||||
if (checkCorrectDate.isValid()) {
|
||||
const checkCorrectDate = new Date(curdate);
|
||||
if (isValidDate(checkCorrectDate)) {
|
||||
return curdate;
|
||||
} else {
|
||||
return ' ';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import { TAPi18n } from '/imports/i18n';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
const DateString = Match.Where(function(dateAsString) {
|
||||
check(dateAsString, String);
|
||||
return moment(dateAsString, moment.ISO_8601).isValid();
|
||||
return isValidDate(new Date(dateAsString));
|
||||
});
|
||||
|
||||
export class TrelloCreator {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,28 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
|
||||
const DateString = Match.Where(function(dateAsString) {
|
||||
check(dateAsString, String);
|
||||
return moment(dateAsString, moment.ISO_8601).isValid();
|
||||
return isValidDate(new Date(dateAsString));
|
||||
});
|
||||
|
||||
export class WekanCreator {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@
|
|||
"markdown-it-mathjax3": "^4.3.2",
|
||||
"meteor-accounts-t9n": "^2.6.0",
|
||||
"meteor-node-stubs": "^1.2.24",
|
||||
"moment": "^2.30.1",
|
||||
"os": "^0.1.2",
|
||||
"papaparse": "^5.5.3",
|
||||
"pretty-ms": "^7.0.1",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,26 @@
|
|||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import moment from 'moment/min/moment-with-locales';
|
||||
import escapeForRegex from 'escape-string-regexp';
|
||||
import Users from '../../models/users';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Boards from '../../models/boards';
|
||||
import Lists from '../../models/lists';
|
||||
import Swimlanes from '../../models/swimlanes';
|
||||
|
|
@ -730,9 +749,7 @@ function findCards(sessionId, query) {
|
|||
userId,
|
||||
modifiedAt: {
|
||||
$lt: new Date(
|
||||
moment()
|
||||
.subtract(1, 'day')
|
||||
.format(),
|
||||
subtract(now(), 1, 'day').toISOString(),
|
||||
),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue