@becollective/utils 1.10.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.semaphore/utils.yml +1 -1
  2. package/index.ts +7 -0
  3. package/lib/FeatureFlag.d.ts +12 -0
  4. package/lib/FeatureFlag.js +51 -0
  5. package/lib/constants/allowedHtmlTags.d.ts +7 -0
  6. package/lib/constants/allowedHtmlTags.js +8 -0
  7. package/lib/constants/avatarColours.d.ts +35 -0
  8. package/lib/constants/avatarColours.js +36 -0
  9. package/lib/constants/countries.d.ts +253 -0
  10. package/lib/constants/countries.js +254 -0
  11. package/lib/constants/currencies.d.ts +29 -0
  12. package/lib/constants/currencies.js +36 -0
  13. package/lib/constants/customFields.d.ts +11 -0
  14. package/lib/constants/customFields.js +12 -0
  15. package/lib/constants/distancesUnits.d.ts +11 -0
  16. package/lib/constants/distancesUnits.js +13 -0
  17. package/lib/constants/groups.d.ts +13 -0
  18. package/lib/constants/groups.js +85 -0
  19. package/lib/constants/insuranceStatuses.d.ts +15 -0
  20. package/lib/constants/insuranceStatuses.js +7 -0
  21. package/lib/constants/insuranceTypes.d.ts +35 -0
  22. package/lib/constants/insuranceTypes.js +36 -0
  23. package/lib/constants/languages.d.ts +2 -0
  24. package/lib/constants/languages.js +194 -0
  25. package/lib/constants/locales.d.ts +119 -0
  26. package/lib/constants/locales.js +191 -0
  27. package/lib/constants/notifications.d.ts +47 -0
  28. package/lib/constants/notifications.js +31 -0
  29. package/lib/constants/opportunities.d.ts +72 -0
  30. package/lib/constants/opportunities.js +82 -0
  31. package/lib/constants/qualificationTypes.d.ts +4 -0
  32. package/lib/constants/qualificationTypes.js +18 -0
  33. package/lib/constants/rules.d.ts +13 -0
  34. package/lib/constants/rules.js +21 -0
  35. package/lib/constants/subscriptions.d.ts +9 -0
  36. package/lib/constants/subscriptions.js +10 -0
  37. package/lib/constants/surveys.d.ts +16 -0
  38. package/lib/constants/surveys.js +17 -0
  39. package/lib/constants.d.ts +1 -0
  40. package/lib/constants.js +76 -0
  41. package/lib/date-time.d.ts +28 -0
  42. package/lib/date-time.js +79 -0
  43. package/lib/forms.d.ts +1 -0
  44. package/lib/forms.js +728 -0
  45. package/lib/locality.d.ts +5 -0
  46. package/lib/locality.js +15 -0
  47. package/lib/money.d.ts +7 -0
  48. package/lib/money.js +32 -0
  49. package/lib/opportunity.d.ts +1 -0
  50. package/lib/opportunity.js +11 -0
  51. package/lib/opportunityUser.d.ts +8 -0
  52. package/lib/opportunityUser.js +29 -0
  53. package/lib/password.d.ts +8 -0
  54. package/lib/password.js +43 -0
  55. package/lib/rrule/date.d.ts +4 -0
  56. package/lib/rrule/date.js +30 -0
  57. package/lib/rrule/index.d.ts +2 -0
  58. package/lib/rrule/index.js +18 -0
  59. package/lib/rrule/parsing.d.ts +6 -0
  60. package/lib/rrule/parsing.js +130 -0
  61. package/lib/rrule/parsing.test.d.ts +1 -0
  62. package/lib/rrule/parsing.test.js +11 -0
  63. package/lib/rrule.d.ts +49 -0
  64. package/lib/rrule.js +303 -0
  65. package/lib/src/FeatureFlag.d.ts +12 -0
  66. package/lib/src/FeatureFlag.js +51 -0
  67. package/lib/src/date-time.d.ts +28 -0
  68. package/lib/src/date-time.js +79 -0
  69. package/lib/src/forms.d.ts +1 -0
  70. package/lib/src/forms.js +728 -0
  71. package/lib/src/locality.d.ts +5 -0
  72. package/lib/src/locality.js +15 -0
  73. package/lib/src/money.d.ts +7 -0
  74. package/lib/src/money.js +32 -0
  75. package/lib/src/opportunity.d.ts +1 -0
  76. package/lib/src/opportunity.js +11 -0
  77. package/lib/src/opportunityUser.d.ts +8 -0
  78. package/lib/src/opportunityUser.js +29 -0
  79. package/lib/src/password.d.ts +8 -0
  80. package/lib/src/password.js +43 -0
  81. package/package.json +15 -29
  82. package/src/FeatureFlag.ts +26 -10
  83. package/src/constants/allowedHtmlTags.ts +6 -0
  84. package/src/constants/avatarColours.ts +34 -0
  85. package/src/constants/countries.ts +252 -0
  86. package/src/constants/currencies.ts +33 -0
  87. package/src/constants/customFields.ts +10 -0
  88. package/src/constants/distancesUnits.ts +11 -0
  89. package/src/constants/groups.ts +83 -0
  90. package/src/constants/insuranceStatuses.ts +5 -0
  91. package/src/constants/insuranceTypes.ts +34 -0
  92. package/src/constants/languages.ts +192 -0
  93. package/src/constants/locales.ts +189 -0
  94. package/src/constants/notifications.ts +30 -0
  95. package/src/constants/opportunities.ts +84 -0
  96. package/src/constants/qualificationTypes.ts +17 -0
  97. package/src/constants/rules.ts +22 -0
  98. package/src/constants/subscriptions.ts +8 -0
  99. package/src/constants/surveys.ts +15 -0
  100. package/src/constants.ts +40 -0
  101. package/src/{date-time.js → date-time.ts} +18 -11
  102. package/src/forms.ts +807 -0
  103. package/src/{opportunityUser.js → opportunityUser.ts} +5 -1
  104. package/src/{password.js → password.ts} +2 -0
  105. package/src/rrule/date.ts +25 -0
  106. package/src/rrule/index.ts +2 -0
  107. package/src/rrule/parsing.ts +136 -0
  108. package/src/rrule.ts +367 -0
  109. package/tests/{date-time.test.js → date-time.test.ts} +48 -46
  110. package/tests/featureFlag.test.ts +36 -39
  111. package/tests/forms.test-data.json +43 -0
  112. package/tests/forms.test-schema.json +144 -0
  113. package/tests/forms.test-uischema.json +117 -0
  114. package/tests/forms.test.ts +674 -0
  115. package/tests/{money.test.js → money.test.ts} +2 -3
  116. package/tests/{password.test.js → password.test.ts} +7 -6
  117. package/tests/rrule/parsing.test.ts +12 -0
  118. package/tests/rrule.test.ts +625 -0
  119. package/tsconfig.json +15 -12
  120. package/bundle.js +0 -270
  121. package/index.js +0 -25
  122. package/rollup.config.js +0 -25
  123. /package/src/{locality.js → locality.ts} +0 -0
  124. /package/src/{money.js → money.ts} +0 -0
  125. /package/src/{opportunity.js → opportunity.ts} +0 -0
@@ -1,4 +1,8 @@
1
- const { pick } = require('lodash');
1
+ const pick = (obj: any, keysToPick: string[] = []) => {
2
+ return Object.fromEntries(
3
+ Object.entries(obj).filter(([key]) => keysToPick.includes(key))
4
+ );
5
+ }
2
6
 
3
7
  /**
4
8
  *
@@ -36,3 +36,5 @@ export const password = {
36
36
  }
37
37
  },
38
38
  };
39
+
40
+ export default password;
@@ -0,0 +1,25 @@
1
+ import { DateTime } from 'luxon';
2
+ import rrule from 'rrule';
3
+
4
+ import { toRruleObj } from './parsing';
5
+
6
+ // Given a start (Date object) and a duration in milliseconds, return a new Date object specifying the end time
7
+ export const getEndTime = (start, duration) => new Date(start.getTime() + duration);
8
+
9
+ // Combine a date and time string in formats: YYYYMMDD, HHss
10
+ export const munge = (date, time) => {
11
+ const datePart = DateTime.fromISO(date).toFormat('YYYYMMDD');
12
+ return `${datePart}${time}`;
13
+ };
14
+
15
+ // Return a date object representing the given date and time string in the timezone specified
16
+ export const transpose = (date, time, timezone) => {
17
+ return DateTime.fromISO(munge(date, time)).setZone(timezone).toDate();
18
+ };
19
+
20
+ // Gets the start time (HHmm format) in the timezone specified for an rrule
21
+ export const getStartTime = (rule, timezone) => {
22
+ // eslint-disable-next-line no-undef
23
+ const obj: any = typeof rule === 'object' ? rrule : toRruleObj(rule);
24
+ return DateTime.fromISO(obj.DTSTART).setZone(timezone).toFormat('HHmm');
25
+ };
@@ -0,0 +1,2 @@
1
+ export * from './parsing';
2
+ export * from './date';
@@ -0,0 +1,136 @@
1
+ import { DateTime } from 'luxon';
2
+
3
+ const omitBy = (obj, predicate) => {
4
+ return Object.fromEntries(
5
+ Object.entries(obj).filter(([key, value]) => !predicate(value, key))
6
+ );
7
+ }
8
+
9
+ export const toRruleObj = function toRruleObj(str) {
10
+ const config = (str || '')
11
+ .split(';')
12
+ .map((component) => component.split('=', 2))
13
+ .reduce((all, [k, v]) => {
14
+ all[k] = v;
15
+ return all;
16
+ }, {});
17
+
18
+ return omitBy(config, (value, key) => !key || value === null);
19
+ };
20
+
21
+ export const toRruleStr = function toRruleStr(obj) {
22
+ const filter = Object.keys(obj).filter((k) => obj[k] !== null && obj[k] !== undefined)
23
+ const map = filter.map((key) => `${key}=${obj[key]}`)
24
+ const join = map.join(';');
25
+ return Object.keys(obj)
26
+ .filter((k) => obj[k] !== null && obj[k] !== undefined)
27
+ .map((key) => `${key}=${obj[key]}`)
28
+ .join(';');
29
+ };
30
+
31
+ const TOKEN_TYPES = {
32
+ SIGN: 'SIGN',
33
+ DAYS: 'DAYS',
34
+ TIME_START: 'TIME_START',
35
+ HOURS: 'HOURS',
36
+ MINUTES: 'MINUTES',
37
+ SECONDS: 'SECONDS',
38
+ WEEKS: 'WEEKS',
39
+ UNKNOWN: 'UNKNOWN',
40
+ };
41
+
42
+ type token = {
43
+ type: string;
44
+ val: number;
45
+ }
46
+
47
+ export const parseDuration = (duration) => {
48
+ // --- lexer
49
+ // follows https://tools.ietf.org/html/rfc5545#section-3.3.6
50
+ let windowStart = 0;
51
+ const tokens: token[] = [];
52
+ for (let windowEnd = 1; windowEnd <= duration.length; windowEnd++) {
53
+ const token = duration.slice(windowStart, windowEnd);
54
+ if (/^[+-]?P$/.test(token)) {
55
+ const val = token[0] === '-' ? -1 : 1;
56
+ tokens.push({ type: TOKEN_TYPES.SIGN, val: val });
57
+ windowStart = windowEnd;
58
+ }
59
+ else if (/^[0-9]*W$/.test(token)) {
60
+ const val = parseInt(token.slice(0, -1), 10);
61
+ tokens.push({ type: TOKEN_TYPES.WEEKS, val: val });
62
+ windowStart = windowEnd;
63
+ }
64
+ else if (/^[0-9]*D$/.test(token)) {
65
+ const val = parseInt(token.slice(0, -1), 10);
66
+ tokens.push({ type: TOKEN_TYPES.DAYS, val: val });
67
+ windowStart = windowEnd;
68
+ }
69
+ else if (token === 'T') {
70
+ tokens.push({ type: TOKEN_TYPES.TIME_START, val: -1 });
71
+ windowStart = windowEnd;
72
+ }
73
+ else if (/^[0-9]*\.?[0-9]*H$/.test(token)) {
74
+ const val = parseFloat(token.slice(0, -1));
75
+ tokens.push({ type: TOKEN_TYPES.HOURS, val: val });
76
+ windowStart = windowEnd;
77
+ }
78
+ else if (/^[0-9]*M$/.test(token)) {
79
+ const val = parseInt(token.slice(0, -1), 10);
80
+ tokens.push({ type: TOKEN_TYPES.MINUTES, val: val });
81
+ windowStart = windowEnd;
82
+ }
83
+ else if (/^[0-9]*S$/.test(token)) {
84
+ const val = parseInt(token.slice(0, -1), 10);
85
+ tokens.push({ type: TOKEN_TYPES.SECONDS, val: val });
86
+ windowStart = windowEnd;
87
+ }
88
+ else if (windowEnd === duration.length) {
89
+ tokens.push({ type: TOKEN_TYPES.UNKNOWN, val: token });
90
+ windowStart = windowEnd;
91
+ }
92
+ }
93
+
94
+ // --- parser
95
+ // this parser is very permissive and doesn't care about token order, repetitions, etc.
96
+ let parsedDurationValue: number = 0;
97
+ // for token type of hours, because we made it support fractions, apply parseInt
98
+ // to round it up to integer
99
+ for (const token of tokens) {
100
+ if (token.type === TOKEN_TYPES.SIGN) parsedDurationValue = token.val;
101
+ else if (token.type === TOKEN_TYPES.WEEKS) parsedDurationValue = token.val * 7 * 24 * 60 * 60 * 1000;
102
+ else if (token.type === TOKEN_TYPES.DAYS) parsedDurationValue = token.val * 24 * 60 * 60 * 1000;
103
+ else if (token.type === TOKEN_TYPES.HOURS) parsedDurationValue = token.val * 60 * 60 * 1000, 10;
104
+ else if (token.type === TOKEN_TYPES.MINUTES) parsedDurationValue = token.val * 60 * 1000;
105
+ else if (token.type === TOKEN_TYPES.SECONDS) parsedDurationValue = token.val * 1000;
106
+ // TODO check if we use this, MILLISECONDS is no longer supported
107
+ // else if (token.type === TOKEN_TYPES.MILLISECONDS) parsedDurationValue = token.val;
108
+ }
109
+
110
+ return parsedDurationValue;
111
+ };
112
+
113
+ export const getDuration = (icalObj) => {
114
+ const duration = icalObj.DURATION;
115
+
116
+ // make it v1 backward compatible
117
+ const dtend = icalObj.DTEND;
118
+ const dtstart = icalObj.DTSTART;
119
+
120
+ if (duration && dtend) {
121
+ return 0;
122
+ }
123
+
124
+ if (duration) {
125
+ return parseDuration(duration);
126
+ }
127
+
128
+ if (dtstart && dtend) {
129
+ const start = DateTime.fromISO(dtstart);
130
+ const end = DateTime.fromISO(dtend);
131
+ const difference = end.diff(start);
132
+ if (!difference.isValid) return 0;
133
+ return difference.toMillis();
134
+ }
135
+ return 0;
136
+ };
package/src/rrule.ts ADDED
@@ -0,0 +1,367 @@
1
+ import { RRule, rrulestr } from 'rrule';
2
+ import { DateTime } from 'luxon';
3
+ import { toRruleObj, toRruleStr, getDuration, getEndTime, getStartTime } from './rrule/index';
4
+ import { transpose } from './rrule/date';
5
+
6
+ type ToFrom = {
7
+ from: Date,
8
+ to: Date,
9
+ }
10
+
11
+ const isEmpty = (obj) => {
12
+ if (Array.isArray(obj) || typeof obj === 'string') {
13
+ return obj.length === 0;
14
+ }
15
+ return !obj;
16
+ }
17
+
18
+ const omit = (obj, keys) => {
19
+ return Object.fromEntries(
20
+ Object.entries(obj).filter(([key]) => !keys.includes(key))
21
+ );
22
+ }
23
+
24
+ const omitBy = (obj, predicate) => {
25
+ return Object.fromEntries(
26
+ Object.entries(obj).filter(([key, value]) => !predicate(value, key))
27
+ );
28
+ }
29
+
30
+ const logError = (...args) => {
31
+ const logger = console.error || console.log;
32
+ logger(...args);
33
+ };
34
+
35
+ export { toRruleObj, toRruleStr };
36
+
37
+ // Customised recurrenceRule parser for BeCollective
38
+ // We store DURATION in our rules, which are not supported by rrule
39
+ // We need to return this data and also remove it from our rrule configuration before parsing it
40
+ export const parse = function parse(str) {
41
+ const icalObj = toRruleObj(str);
42
+ const duration = getDuration(icalObj);
43
+ // TODO delete unused?
44
+ // const until = icalObj.UNTIL;
45
+ const dtend = icalObj.DTEND;
46
+ const nondurationItems = omit(icalObj, ['DURATION', 'DTEND']);
47
+ const rrule = rrulestr(toRruleStr(omitBy(nondurationItems, isEmpty)));
48
+ return { duration, durationString: icalObj.DURATION, rrule, dtend };
49
+ };
50
+
51
+ export const validateRruleConfig = (obj) => {
52
+ const config = obj || {};
53
+ const REQUIRED_FIELDS = [
54
+ { field: 'DTSTART', name: 'Start time' },
55
+ { field: 'RRULE:FREQ', name: 'Frequency (Month/Weekly)' },
56
+ { field: 'INTERVAL', name: 'Interval' },
57
+ { field: 'UNTIL', name: 'Until' },
58
+ { field: 'Duration', name: 'Duration' },
59
+ ];
60
+
61
+ const errors: string[] = [];
62
+
63
+ REQUIRED_FIELDS.forEach((fieldConfig) => {
64
+ if (!config[fieldConfig.field]) {
65
+ errors.push(`Please enter a ${fieldConfig.name}`);
66
+ }
67
+ });
68
+
69
+ if (config.DTSTART && config.UNTIL && DateTime.fromISO(config.DTSTART) > DateTime.fromISO(config.UNTIL)) {
70
+ errors.push('DTSTART must be earlier than or equal to UNTIL');
71
+ }
72
+
73
+ return errors;
74
+ };
75
+
76
+ export const validateRruleConfigV2 = (bcRruleInstance) => {
77
+ const rruleInstance = bcRruleInstance.rrule;
78
+ const REQUIRED_RRULE_FIELDS = [
79
+ { field: 'dtstart', name: 'Start time' },
80
+ { field: 'interval', name: 'Interval' },
81
+ { field: 'until', name: 'Until' },
82
+ ];
83
+
84
+ const REQUIRED_BC_FIELDS = [{ field: 'duration', name: 'Duration' }];
85
+
86
+ const REQUIRED_BC_FREQ = {
87
+ field: 'freq',
88
+ name: 'Frequency (Month/Weekly/Yearly)',
89
+ values: [RRule.YEARLY, RRule.WEEKLY, RRule.MONTHLY],
90
+ };
91
+
92
+ const errors: string[] = [];
93
+
94
+ REQUIRED_RRULE_FIELDS.forEach((fieldConfig) => {
95
+ if (!rruleInstance.options[fieldConfig.field]) {
96
+ errors.push(`Please enter a ${fieldConfig.name}`);
97
+ }
98
+ });
99
+
100
+ if (!REQUIRED_BC_FREQ.values.includes(rruleInstance.options[REQUIRED_BC_FREQ.field])) {
101
+ errors.push(`Please enter a ${REQUIRED_BC_FREQ.name}`);
102
+ }
103
+
104
+ REQUIRED_BC_FIELDS.forEach((fieldConfig) => {
105
+ if (!bcRruleInstance[fieldConfig.field]) {
106
+ errors.push(`Please enter a ${fieldConfig.name}`);
107
+ }
108
+ });
109
+
110
+ if (
111
+ rruleInstance.options.dtstart &&
112
+ rruleInstance.options.until &&
113
+ DateTime.fromISO(rruleInstance.options.dtstart) > DateTime.fromISO(rruleInstance.options.until)
114
+ ) {
115
+ errors.push('DTSTART must be earlier than or equal to UNTIL');
116
+ }
117
+
118
+ return errors;
119
+ };
120
+
121
+ /**
122
+ * a helper function that put offical rrule and becollective rrule fields together
123
+ * @param {Object} rruleInstance an RRule instance.
124
+ * @param {*} duration non rrule standard duration field.
125
+ */
126
+ export const buildBcRruleString = (rruleInstance, duration = '0H') => {
127
+ let rruleString = rruleInstance.toString();
128
+ if (duration) {
129
+ rruleString = `${rruleString};DURATION=${duration}`;
130
+ }
131
+ const bcRrule = parse(rruleString);
132
+ const errors = validateRruleConfigV2(bcRrule);
133
+ if (errors.length === 0) {
134
+ return rruleString;
135
+ }
136
+ else {
137
+ throw new Error(JSON.stringify(errors));
138
+ }
139
+ };
140
+
141
+ export const isValidRrule = (obj) => validateRruleConfig(obj).length === 0;
142
+
143
+ /* === TIMEZONE ISSUES WITH RRULE ===
144
+
145
+ The rrule library does not store, use or account for timezone. The timezone information is lost and this leads to bugs displaying times and with
146
+ generating recurrences. This happens because rrule determines the start time by interpreting DTSTART in the timezone of the runtime
147
+ environment. This is fine when the runtime and an opportunity share the same timezone. However, inconsistencies occur when they
148
+ differ, and daylight savings is introduced.
149
+
150
+ INCONSISTENT START & END TIMES
151
+ -------------------------------------------
152
+ We want to specify a start and end time (5pm - 7pm)
153
+ Technically we need to express this start and end time as UTC timestamps (2017-07-03 T07:00 - 2017-07-03 T09:00)
154
+
155
+ This presents a problem when we interpret rrule strings to display and generate recurrences.
156
+
157
+ Consider that we want a recurrenceRule to apply for ‘Asia/Taipei (+0800 UTC)’ and start at 1500 hours, we would have the following
158
+ UTC timestamp:
159
+
160
+ 2017-07-03 T07:00
161
+
162
+ If our runtime is using Australia/Melbourne time, rrule would determine the start time of each recurrence to be at T17:00
163
+
164
+ In Asia/Taipei time, that is T15:00 which is correct
165
+
166
+ However, fast forward to October when daylight savings kicks in for Australia/Melbourne, and suddenly T17:00 translates to T14:00
167
+ in Asia/Taipei.
168
+
169
+ As illustrated above, we need to workaround the rrule library by incorporating timezone information in the way we display and
170
+ generate recurrences.
171
+
172
+ OFF BY ONE DATES
173
+ ———————————
174
+ This bug occurs because the start time is determined using runtime’s timezone. When finding recurrences between a set of dates,
175
+ the correct dates and times are found for the runtime’s local timezone. But when the client uses these dates, the timezone
176
+ conversion could cause the date to change.
177
+
178
+ Example generated start time: 2017-10-10 T20:30 UTC
179
+ If a Melbourne client interprets this timestamp, it would translate to: 2017-10-11 T:07:30 AEST
180
+
181
+ So time is correct but date is WRONG
182
+
183
+
184
+ --- SUMMARY & TLDR ---;
185
+ The rrule spec causes us to lose timezone information and leads to inconsistent interpretations of DTSTART and UNTIL times. This is
186
+ further complicated by the fact that a rule is parsed and interpreted based on code runtime’s timezone, which means we are
187
+ potentially shuffling and converting things between 3 timezones: UTC -> runtime -> opportunity. Hence we must implement several
188
+ “tricks” so that we ignore the time portion from dates and the date portion from times.
189
+
190
+ ***********************************************************/
191
+
192
+ // Return correct and consistent human readable values for a recurrenceRule
193
+ // We need to ignore DTSTART date component and use only the time component so start time is updated according to daylight
194
+ // savings
195
+ export const recurrenceToText = (recurrenceRule, timezone, isFlexible = false) => {
196
+ const TIME_FORMAT = 'h:mm a';
197
+ try {
198
+ const { duration, rrule: rule } = parse(recurrenceRule);
199
+
200
+ const dtstartToTimezone = DateTime.fromJSDate(rule.origOptions.dtstart as Date).setZone(timezone);
201
+ const untilToTimezone = DateTime.fromJSDate(rule.origOptions.until as Date).setZone(timezone);
202
+ const rrule = new RRule({
203
+ ...rule.origOptions,
204
+ dtstart:
205
+ DateTime.utc(
206
+ dtstartToTimezone.year,
207
+ dtstartToTimezone.month,
208
+ dtstartToTimezone.day,
209
+ dtstartToTimezone.hour,
210
+ dtstartToTimezone.minute,
211
+ dtstartToTimezone.second
212
+ )
213
+ .toJSDate(),
214
+ tzid: timezone,
215
+ until:
216
+ DateTime.utc(
217
+ untilToTimezone.year,
218
+ untilToTimezone.month,
219
+ untilToTimezone.day,
220
+ untilToTimezone.hour,
221
+ untilToTimezone.minute,
222
+ untilToTimezone.second
223
+ )
224
+ .toJSDate(),
225
+ });
226
+ if (isFlexible) {
227
+ const hours = duration / 1000 / 60 / 60;
228
+ let ruleText = rrule.toText();
229
+ if (rule.origOptions.count === 1) {
230
+ // When it is not a repeating rule
231
+ ruleText = ruleText.replace(/every year until/i, 'in total by');
232
+ ruleText = ruleText.replace(/ \(~ approximate\)/i, '');
233
+ }
234
+ return {
235
+ text: `${hours} hours ${ruleText}`,
236
+ timezone,
237
+ };
238
+ }
239
+ // Get today's start time in the recurrenceRule's timezone, then convert that timestamp to our local time
240
+ const end = dtstartToTimezone.plus(duration);
241
+ return {
242
+ text: rrule.toText().charAt(0).toUpperCase() + rrule.toText().slice(1),
243
+ time: `${dtstartToTimezone.toFormat(TIME_FORMAT)} to ${end.toFormat(TIME_FORMAT)}`,
244
+ timezone,
245
+ };
246
+ }
247
+ catch (e) {
248
+ return { text: '', time: '', timezone };
249
+ }
250
+ };
251
+
252
+ // Return the text based on the specified timezone
253
+ // This method doesn't convert rrule to it's owner opportunity's timezone
254
+ export const recurrenceToTextInTimezone = (recurrenceRule, timezone) => {
255
+ const TIME_FORMAT = 'h:mm A';
256
+
257
+ const localTimezone = timezone || 'UTC';
258
+
259
+ try {
260
+ const { duration, rrule: rule } = parse(recurrenceRule);
261
+ const start = DateTime.fromISO(getStartTime(recurrenceRule, localTimezone), 'HHmm').setZone(localTimezone);
262
+ const end = DateTime.fromISO(getEndTime(start.toJSDate(), duration)).setZone(localTimezone);
263
+ return {
264
+ text: rule.toText().charAt(0).toUpperCase() + rule.toText().slice(1),
265
+ time: `${start.toFormat(TIME_FORMAT)} to ${end.toFormat(TIME_FORMAT)}`,
266
+ localTimezone,
267
+ };
268
+ }
269
+ catch (e) {
270
+ return { text: '', time: '', localTimezone };
271
+ }
272
+ };
273
+
274
+ export const recurrencesBetween = (fromDate, toDate, recurrenceRule, timezone) => {
275
+ try {
276
+ // We need to deal with time and dates separately because timestamps will be converted and lead to errors
277
+ const time = getStartTime(recurrenceRule, timezone);
278
+ const { duration, rrule: rule } = parse(recurrenceRule);
279
+ const recurrences = rule.between(fromDate, toDate);
280
+
281
+ return recurrences.map((start) => {
282
+ // eslint-disable-next-line no-undef
283
+ const fixed = transpose(start, time, timezone);
284
+ return {
285
+ from: fixed,
286
+ to: getEndTime(fixed, duration),
287
+ };
288
+ });
289
+ }
290
+ catch (e) {
291
+ logError('Error while generating recurrences between', fromDate.toString(), 'to', toDate.toString());
292
+ logError('RRULE:', recurrenceRule, 'TIMEZONE:', timezone);
293
+ logError('***\n\n', e, '\n\n***');
294
+ return [];
295
+ }
296
+ };
297
+
298
+ /**
299
+ *
300
+ * @param {*} rrule
301
+ * @param {*} timezone
302
+ * @param {*} from
303
+ * @param {*} isFlexible
304
+ * @returns an array of date in true utc (DST offset is considered)
305
+ */
306
+ export const recurrencesBetweenV2 = (rrule, timezone, from = new Date(), isFlexible = false) => {
307
+ const { duration, rrule: rule } = parse(rrule);
308
+
309
+ const dtstartToTimezone = DateTime.fromJSDate(rule.origOptions.dtstart).setZone(timezone);
310
+ const untilToTimezone = DateTime.fromJSDate(rule.origOptions.until).setZone(timezone);
311
+ const rruleToConvert = new RRule({
312
+ ...rule.origOptions,
313
+ dtstart:
314
+ DateTime.utc(
315
+ dtstartToTimezone.year,
316
+ dtstartToTimezone.month,
317
+ dtstartToTimezone.day,
318
+ dtstartToTimezone.hour,
319
+ dtstartToTimezone.minute,
320
+ dtstartToTimezone.second
321
+ ).toJSDate(),
322
+ tzid: timezone,
323
+ until:
324
+ DateTime.utc(
325
+ untilToTimezone.year,
326
+ untilToTimezone.month,
327
+ untilToTimezone.day,
328
+ untilToTimezone.hour,
329
+ untilToTimezone.minute,
330
+ untilToTimezone.second
331
+ ).toJSDate(),
332
+ });
333
+
334
+ const occurrences = rruleToConvert
335
+ .all()
336
+ .map((date) => DateTime.fromJSDate(date).toUTC().setZone('local', { keepLocalTime: true }));
337
+
338
+ if (!occurrences.length) {
339
+ return [];
340
+ }
341
+
342
+ const output: ToFrom[] = [];
343
+ if(isFlexible) {
344
+ let i = 0;
345
+ occurrences.map(o => {
346
+ output.push({ from: o.toJSDate(), to: o.toJSDate() });
347
+ // update "to" field
348
+ if (i > 0) {
349
+ output[i - 1].to = o.toUTC().minus({ seconds: 1 }).toJSDate();
350
+ }
351
+ if (i === occurrences.length - 1) {
352
+ output[i].to = DateTime.fromJSDate(rule.options.until as Date).toUTC().minus({ seconds: 1 }).toJSDate();
353
+ }
354
+ i++;
355
+ });
356
+ return output.filter(((o: ToFrom) => from ? DateTime.fromJSDate(o.to) > DateTime.fromJSDate(from) : DateTime.fromJSDate(o.to) < DateTime.fromJSDate(rule.options.until as Date)));
357
+ } else {
358
+ const output: ToFrom[] = [];
359
+ occurrences.map(o => {
360
+ output.push({
361
+ from: o.toJSDate(),
362
+ to: o.toUTC().plus(duration).toJSDate(),
363
+ });
364
+ });
365
+ return output.filter(((o: ToFrom) => from ? DateTime.fromJSDate(o.to) > DateTime.fromJSDate(from) : DateTime.fromJSDate(o.to) < DateTime.fromJSDate(rule.options.until as Date))) as { from: Date, to: Date }[];
366
+ }
367
+ };