@praxisui/cron-builder 6.0.0-beta.0 → 8.0.0-beta.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.
- package/README.md +3 -28
- package/fesm2022/praxisui-cron-builder.mjs +409 -46
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -93,35 +93,10 @@ Inputs/Outputs:
|
|
|
93
93
|
- `previewOccurrences?: number` – number of preview dates to show.
|
|
94
94
|
- `validators?: { invalidCronMessage?: string }` – customize error messages.
|
|
95
95
|
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
This component dynamically imports `cron-parser`, `cronstrue` and `cron-validator`. Some versions of these, or their transitive deps (e.g. `luxon`), may emit CommonJS warnings in Angular builds.
|
|
99
|
-
|
|
100
|
-
If you want to suppress warnings in your app build, add them to `allowedCommonJsDependencies`:
|
|
101
|
-
|
|
102
|
-
```json
|
|
103
|
-
// angular.json
|
|
104
|
-
{
|
|
105
|
-
"projects": {
|
|
106
|
-
"your-app": {
|
|
107
|
-
"architect": {
|
|
108
|
-
"build": {
|
|
109
|
-
"options": {
|
|
110
|
-
"allowedCommonJsDependencies": [
|
|
111
|
-
"cron-parser",
|
|
112
|
-
"cronstrue",
|
|
113
|
-
"cron-validator",
|
|
114
|
-
"luxon"
|
|
115
|
-
]
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
96
|
+
## Build Notes
|
|
123
97
|
|
|
124
|
-
|
|
98
|
+
The builder resolves validation, humanized descriptions and occurrence preview inside the library runtime.
|
|
99
|
+
Apps consuming `@praxisui/cron-builder` should not need `allowedCommonJsDependencies` entries just to use the component.
|
|
125
100
|
|
|
126
101
|
## Compatibility
|
|
127
102
|
|
|
@@ -30,10 +30,405 @@ import { PraxisIconDirective } from '@praxisui/core';
|
|
|
30
30
|
import { Subject } from 'rxjs';
|
|
31
31
|
import { takeUntil } from 'rxjs/operators';
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const MONTH_NAMES = {
|
|
34
|
+
JAN: 1,
|
|
35
|
+
FEB: 2,
|
|
36
|
+
MAR: 3,
|
|
37
|
+
APR: 4,
|
|
38
|
+
MAY: 5,
|
|
39
|
+
JUN: 6,
|
|
40
|
+
JUL: 7,
|
|
41
|
+
AUG: 8,
|
|
42
|
+
SEP: 9,
|
|
43
|
+
OCT: 10,
|
|
44
|
+
NOV: 11,
|
|
45
|
+
DEC: 12,
|
|
46
|
+
};
|
|
47
|
+
const WEEKDAY_NAMES = {
|
|
48
|
+
SUN: 0,
|
|
49
|
+
MON: 1,
|
|
50
|
+
TUE: 2,
|
|
51
|
+
WED: 3,
|
|
52
|
+
THU: 4,
|
|
53
|
+
FRI: 5,
|
|
54
|
+
SAT: 6,
|
|
55
|
+
};
|
|
56
|
+
const WEEKDAY_LABELS_EN = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
57
|
+
const zonedFormatters = new Map();
|
|
58
|
+
const timeFormatters = new Map();
|
|
59
|
+
function getTimeFormatter(locale) {
|
|
60
|
+
const key = locale || 'en-US';
|
|
61
|
+
if (!timeFormatters.has(key)) {
|
|
62
|
+
timeFormatters.set(key, new Intl.DateTimeFormat(key, {
|
|
63
|
+
hour: 'numeric',
|
|
64
|
+
minute: '2-digit',
|
|
65
|
+
hour12: true,
|
|
66
|
+
timeZone: 'UTC',
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
return timeFormatters.get(key);
|
|
70
|
+
}
|
|
71
|
+
function getZonedFormatter(timeZone) {
|
|
72
|
+
const key = timeZone || 'UTC';
|
|
73
|
+
if (!zonedFormatters.has(key)) {
|
|
74
|
+
zonedFormatters.set(key, new Intl.DateTimeFormat('en-US', {
|
|
75
|
+
timeZone: key,
|
|
76
|
+
year: 'numeric',
|
|
77
|
+
month: '2-digit',
|
|
78
|
+
day: '2-digit',
|
|
79
|
+
hour: '2-digit',
|
|
80
|
+
minute: '2-digit',
|
|
81
|
+
second: '2-digit',
|
|
82
|
+
weekday: 'short',
|
|
83
|
+
hour12: false,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
return zonedFormatters.get(key);
|
|
87
|
+
}
|
|
88
|
+
function normalizeToken(token) {
|
|
89
|
+
return token.trim().toUpperCase();
|
|
90
|
+
}
|
|
91
|
+
function normalizeValue(raw, aliases) {
|
|
92
|
+
const normalized = normalizeToken(raw);
|
|
93
|
+
return normalized.replace(/[A-Z]{3}/g, (match) => {
|
|
94
|
+
const resolved = aliases[match];
|
|
95
|
+
return resolved == null ? match : String(resolved);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function makeAnyField() {
|
|
99
|
+
return { any: true, values: new Set(), nthWeekday: null };
|
|
100
|
+
}
|
|
101
|
+
function parseSimpleNumber(value, min, max) {
|
|
102
|
+
if (!/^\d+$/.test(value)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const parsed = Number(value);
|
|
106
|
+
if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
function addRange(values, start, end, step, min, max) {
|
|
112
|
+
if (step < 1 || start > end || start < min || end > max) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
for (let current = start; current <= end; current += step) {
|
|
116
|
+
values.add(current);
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
function parseCronField(raw, min, max, aliases = {}, options) {
|
|
121
|
+
const token = normalizeValue(raw, aliases);
|
|
122
|
+
if (token === '*' || (options?.allowQuestion && token === '?')) {
|
|
123
|
+
return makeAnyField();
|
|
124
|
+
}
|
|
125
|
+
const field = { any: false, values: new Set(), nthWeekday: null };
|
|
126
|
+
for (const piece of token.split(',')) {
|
|
127
|
+
const current = piece.trim();
|
|
128
|
+
if (!current) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (options?.allowNthWeekday && current.includes('#')) {
|
|
132
|
+
const [weekdayRaw, nthRaw] = current.split('#');
|
|
133
|
+
const weekday = parseSimpleNumber(weekdayRaw, min, max);
|
|
134
|
+
const nth = parseSimpleNumber(nthRaw, 1, 5);
|
|
135
|
+
if (weekday == null || nth == null) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
field.nthWeekday = {
|
|
139
|
+
weekday: options.weekdayField ? normalizeWeekday(weekday) : weekday,
|
|
140
|
+
nth,
|
|
141
|
+
};
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const [base, stepRaw] = current.split('/');
|
|
145
|
+
const step = stepRaw == null ? 1 : parseSimpleNumber(stepRaw, 1, max - min + 1);
|
|
146
|
+
if (step == null) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
if (base === '*') {
|
|
150
|
+
if (!addRange(field.values, min, max, step, min, max)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (base.includes('-')) {
|
|
156
|
+
const [startRaw, endRaw] = base.split('-');
|
|
157
|
+
const start = parseSimpleNumber(startRaw, min, max);
|
|
158
|
+
const end = parseSimpleNumber(endRaw, min, max);
|
|
159
|
+
if (start == null || end == null) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (!addRange(field.values, normalizeWeekday(start, options?.weekdayField), normalizeWeekday(end, options?.weekdayField), step, min, max)) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const single = parseSimpleNumber(base, min, max);
|
|
168
|
+
if (single == null) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const normalizedSingle = normalizeWeekday(single, options?.weekdayField);
|
|
172
|
+
if (stepRaw == null) {
|
|
173
|
+
field.values.add(normalizedSingle);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!addRange(field.values, normalizedSingle, max, step, min, max)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return field.values.size > 0 || field.nthWeekday != null ? field : null;
|
|
181
|
+
}
|
|
182
|
+
function normalizeWeekday(value, weekdayField = false) {
|
|
183
|
+
if (!weekdayField) {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
return value === 7 ? 0 : value;
|
|
187
|
+
}
|
|
188
|
+
function getZonedDateParts(date, timeZone) {
|
|
189
|
+
const formatter = getZonedFormatter(timeZone || 'UTC');
|
|
190
|
+
const parts = formatter.formatToParts(date);
|
|
191
|
+
const record = Object.fromEntries(parts.map((part) => [part.type, part.value]));
|
|
192
|
+
return {
|
|
193
|
+
year: Number(record.year),
|
|
194
|
+
month: Number(record.month),
|
|
195
|
+
day: Number(record.day),
|
|
196
|
+
hour: Number(record.hour),
|
|
197
|
+
minute: Number(record.minute),
|
|
198
|
+
second: Number(record.second),
|
|
199
|
+
weekday: WEEKDAY_NAMES[record.weekday.toUpperCase()] ?? 0,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function isNthWeekdayInMonth(parts, nthWeekday) {
|
|
203
|
+
if (parts.weekday !== nthWeekday.weekday) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const occurrence = Math.floor((parts.day - 1) / 7) + 1;
|
|
207
|
+
return occurrence === nthWeekday.nth;
|
|
208
|
+
}
|
|
209
|
+
function matchesField(field, value, parts) {
|
|
210
|
+
if (field.any) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (field.nthWeekday) {
|
|
214
|
+
return isNthWeekdayInMonth(parts, field.nthWeekday);
|
|
215
|
+
}
|
|
216
|
+
return field.values.has(value);
|
|
217
|
+
}
|
|
218
|
+
function matchesDay(parsed, parts) {
|
|
219
|
+
const domAny = parsed.dayOfMonth.any;
|
|
220
|
+
const dowAny = parsed.dayOfWeek.any && parsed.dayOfWeek.nthWeekday == null;
|
|
221
|
+
const domMatch = matchesField(parsed.dayOfMonth, parts.day, parts);
|
|
222
|
+
const dowMatch = matchesField(parsed.dayOfWeek, parts.weekday, parts);
|
|
223
|
+
if (domAny && dowAny) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (domAny) {
|
|
227
|
+
return dowMatch;
|
|
228
|
+
}
|
|
229
|
+
if (dowAny) {
|
|
230
|
+
return domMatch;
|
|
231
|
+
}
|
|
232
|
+
return domMatch || dowMatch;
|
|
233
|
+
}
|
|
234
|
+
function parseCronExpression(expression) {
|
|
235
|
+
const trimmed = expression.trim();
|
|
236
|
+
if (!trimmed) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const parts = trimmed.split(/\s+/);
|
|
240
|
+
if (parts.length !== 5 && parts.length !== 6) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const hasSeconds = parts.length === 6;
|
|
244
|
+
const [secondsRaw, minutesRaw, hoursRaw, domRaw, monthRaw, dowRaw] = hasSeconds
|
|
245
|
+
? parts
|
|
246
|
+
: ['0', ...parts];
|
|
247
|
+
const seconds = parseCronField(secondsRaw, 0, 59);
|
|
248
|
+
const minutes = parseCronField(minutesRaw, 0, 59);
|
|
249
|
+
const hours = parseCronField(hoursRaw, 0, 23);
|
|
250
|
+
const dayOfMonth = parseCronField(domRaw, 1, 31, {}, { allowQuestion: true });
|
|
251
|
+
const month = parseCronField(monthRaw, 1, 12, MONTH_NAMES);
|
|
252
|
+
const dayOfWeek = parseCronField(dowRaw, 0, 7, WEEKDAY_NAMES, {
|
|
253
|
+
allowQuestion: true,
|
|
254
|
+
allowNthWeekday: true,
|
|
255
|
+
weekdayField: true,
|
|
256
|
+
});
|
|
257
|
+
if (!seconds || !minutes || !hours || !dayOfMonth || !month || !dayOfWeek) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
raw: trimmed,
|
|
262
|
+
hasSeconds,
|
|
263
|
+
seconds,
|
|
264
|
+
minutes,
|
|
265
|
+
hours,
|
|
266
|
+
dayOfMonth,
|
|
267
|
+
month,
|
|
268
|
+
dayOfWeek,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function isValidCronExpression(expression) {
|
|
272
|
+
return parseCronExpression(expression) != null;
|
|
273
|
+
}
|
|
274
|
+
function formatTime(hours, minutes, locale) {
|
|
275
|
+
const seed = new Date(Date.UTC(2024, 0, 1, hours, minutes, 0));
|
|
276
|
+
return getTimeFormatter(locale || 'en-US').format(seed);
|
|
277
|
+
}
|
|
278
|
+
function formatWeekdayList(days) {
|
|
279
|
+
if (days.length === 0) {
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
if (days.length === 2 && days[0] + 1 === days[1]) {
|
|
283
|
+
return `${WEEKDAY_LABELS_EN[days[0]]} through ${WEEKDAY_LABELS_EN[days[1]]}`;
|
|
284
|
+
}
|
|
285
|
+
if (days.length === 5 && days.join(',') === '1,2,3,4,5') {
|
|
286
|
+
return 'Monday through Friday';
|
|
287
|
+
}
|
|
288
|
+
if (days.length === 7) {
|
|
289
|
+
return 'every day';
|
|
290
|
+
}
|
|
291
|
+
return days.map((day) => WEEKDAY_LABELS_EN[day]).join(', ');
|
|
292
|
+
}
|
|
293
|
+
function ordinal(value) {
|
|
294
|
+
if (value % 100 >= 11 && value % 100 <= 13) {
|
|
295
|
+
return `${value}th`;
|
|
296
|
+
}
|
|
297
|
+
switch (value % 10) {
|
|
298
|
+
case 1:
|
|
299
|
+
return `${value}st`;
|
|
300
|
+
case 2:
|
|
301
|
+
return `${value}nd`;
|
|
302
|
+
case 3:
|
|
303
|
+
return `${value}rd`;
|
|
304
|
+
default:
|
|
305
|
+
return `${value}th`;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function humanizeCronExpression(expression, locale = 'en-US') {
|
|
309
|
+
const parsed = parseCronExpression(expression);
|
|
310
|
+
if (!parsed) {
|
|
311
|
+
return '';
|
|
312
|
+
}
|
|
313
|
+
const minutes = parsed.minutes.values;
|
|
314
|
+
const hours = parsed.hours.values;
|
|
315
|
+
const dom = parsed.dayOfMonth.values;
|
|
316
|
+
const dow = parsed.dayOfWeek.values;
|
|
317
|
+
const nthWeekday = parsed.dayOfWeek.nthWeekday;
|
|
318
|
+
const secondsIsZero = parsed.seconds.any || parsed.seconds.values.size === 1 && parsed.seconds.values.has(0);
|
|
319
|
+
if (secondsIsZero &&
|
|
320
|
+
parsed.minutes.any &&
|
|
321
|
+
parsed.hours.any &&
|
|
322
|
+
parsed.dayOfMonth.any &&
|
|
323
|
+
parsed.month.any &&
|
|
324
|
+
parsed.dayOfWeek.any) {
|
|
325
|
+
return 'Every minute';
|
|
326
|
+
}
|
|
327
|
+
if (secondsIsZero &&
|
|
328
|
+
!parsed.minutes.any &&
|
|
329
|
+
minutes.size > 1 &&
|
|
330
|
+
hours.size === 24 &&
|
|
331
|
+
parsed.dayOfMonth.any &&
|
|
332
|
+
parsed.month.any &&
|
|
333
|
+
parsed.dayOfWeek.any) {
|
|
334
|
+
const orderedMinutes = Array.from(minutes).sort((a, b) => a - b);
|
|
335
|
+
const step = orderedMinutes[1] - orderedMinutes[0];
|
|
336
|
+
const isRegularStep = orderedMinutes.every((value, index) => index === 0 || value - orderedMinutes[index - 1] === step);
|
|
337
|
+
if (isRegularStep && orderedMinutes[0] === 0) {
|
|
338
|
+
return `Every ${step} minutes`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (parsed.seconds.any &&
|
|
342
|
+
!parsed.minutes.any &&
|
|
343
|
+
minutes.size === 1 &&
|
|
344
|
+
!parsed.hours.any &&
|
|
345
|
+
hours.size === 1 &&
|
|
346
|
+
parsed.month.any &&
|
|
347
|
+
parsed.dayOfMonth.any &&
|
|
348
|
+
parsed.dayOfWeek.any) {
|
|
349
|
+
return `At ${formatTime(Array.from(hours)[0], Array.from(minutes)[0], locale)}`;
|
|
350
|
+
}
|
|
351
|
+
if (minutes.size === 1 && hours.size === 1 && parsed.month.any && parsed.dayOfMonth.any && parsed.dayOfWeek.any) {
|
|
352
|
+
return `At ${formatTime(Array.from(hours)[0], Array.from(minutes)[0], locale)}`;
|
|
353
|
+
}
|
|
354
|
+
if (minutes.size === 1 && hours.size === 1 && parsed.month.any && parsed.dayOfMonth.any && !parsed.dayOfWeek.any && nthWeekday == null) {
|
|
355
|
+
const orderedDays = Array.from(dow).sort((a, b) => a - b);
|
|
356
|
+
return `At ${formatTime(Array.from(hours)[0], Array.from(minutes)[0], locale)}, ${formatWeekdayList(orderedDays)}`;
|
|
357
|
+
}
|
|
358
|
+
if (minutes.size === 1 && hours.size === 1 && parsed.month.any && !parsed.dayOfMonth.any && parsed.dayOfWeek.any) {
|
|
359
|
+
return `At ${formatTime(Array.from(hours)[0], Array.from(minutes)[0], locale)}, on day ${Array.from(dom)[0]} of the month`;
|
|
360
|
+
}
|
|
361
|
+
if (minutes.size === 1 && hours.size === 1 && parsed.month.any && nthWeekday) {
|
|
362
|
+
return `At ${formatTime(Array.from(hours)[0], Array.from(minutes)[0], locale)}, on the ${ordinal(nthWeekday.nth)} ${WEEKDAY_LABELS_EN[nthWeekday.weekday]} of the month`;
|
|
363
|
+
}
|
|
364
|
+
const fields = parsed.hasSeconds
|
|
365
|
+
? [
|
|
366
|
+
fieldToString(parsed.seconds),
|
|
367
|
+
fieldToString(parsed.minutes),
|
|
368
|
+
fieldToString(parsed.hours),
|
|
369
|
+
fieldToString(parsed.dayOfMonth),
|
|
370
|
+
fieldToString(parsed.month),
|
|
371
|
+
fieldToString(parsed.dayOfWeek),
|
|
372
|
+
]
|
|
373
|
+
: [
|
|
374
|
+
fieldToString(parsed.minutes),
|
|
375
|
+
fieldToString(parsed.hours),
|
|
376
|
+
fieldToString(parsed.dayOfMonth),
|
|
377
|
+
fieldToString(parsed.month),
|
|
378
|
+
fieldToString(parsed.dayOfWeek),
|
|
379
|
+
];
|
|
380
|
+
return `Runs on schedule ${fields.join(' ')}`;
|
|
381
|
+
}
|
|
382
|
+
function fieldToString(field) {
|
|
383
|
+
if (field.any) {
|
|
384
|
+
return '*';
|
|
385
|
+
}
|
|
386
|
+
if (field.nthWeekday) {
|
|
387
|
+
return `${field.nthWeekday.weekday}#${field.nthWeekday.nth}`;
|
|
388
|
+
}
|
|
389
|
+
return Array.from(field.values).sort((a, b) => a - b).join(',');
|
|
390
|
+
}
|
|
391
|
+
function roundCandidate(date, hasSeconds) {
|
|
392
|
+
const rounded = new Date(date.getTime());
|
|
393
|
+
if (hasSeconds) {
|
|
394
|
+
rounded.setMilliseconds(0);
|
|
395
|
+
rounded.setSeconds(rounded.getSeconds() + 1);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
rounded.setSeconds(0, 0);
|
|
399
|
+
rounded.setMinutes(rounded.getMinutes() + 1);
|
|
400
|
+
}
|
|
401
|
+
return rounded;
|
|
402
|
+
}
|
|
403
|
+
function matchesCron(parsed, date, timeZone) {
|
|
404
|
+
const parts = getZonedDateParts(date, timeZone || 'UTC');
|
|
405
|
+
return (matchesField(parsed.month, parts.month, parts) &&
|
|
406
|
+
matchesDay(parsed, parts) &&
|
|
407
|
+
matchesField(parsed.hours, parts.hour, parts) &&
|
|
408
|
+
matchesField(parsed.minutes, parts.minute, parts) &&
|
|
409
|
+
matchesField(parsed.seconds, parts.second, parts));
|
|
410
|
+
}
|
|
411
|
+
function getNextCronOccurrences(expression, count, options) {
|
|
412
|
+
const parsed = parseCronExpression(expression);
|
|
413
|
+
if (!parsed || count <= 0) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
const timeZone = options?.timeZone || 'UTC';
|
|
417
|
+
const results = [];
|
|
418
|
+
const stepMs = parsed.hasSeconds ? 1000 : 60_000;
|
|
419
|
+
let cursor = roundCandidate(options?.currentDate ?? new Date(), parsed.hasSeconds);
|
|
420
|
+
let iterations = 0;
|
|
421
|
+
const maxIterations = parsed.hasSeconds ? 250_000 : 50_000;
|
|
422
|
+
while (results.length < count && iterations < maxIterations) {
|
|
423
|
+
if (matchesCron(parsed, cursor, timeZone)) {
|
|
424
|
+
results.push(new Date(cursor.getTime()));
|
|
425
|
+
}
|
|
426
|
+
cursor = new Date(cursor.getTime() + stepMs);
|
|
427
|
+
iterations += 1;
|
|
428
|
+
}
|
|
429
|
+
return results;
|
|
430
|
+
}
|
|
431
|
+
|
|
37
432
|
class PdxCronBuilderComponent {
|
|
38
433
|
// Tipos auxiliares para Typed Forms
|
|
39
434
|
fb = inject(NonNullableFormBuilder);
|
|
@@ -201,58 +596,26 @@ class PdxCronBuilderComponent {
|
|
|
201
596
|
}
|
|
202
597
|
}
|
|
203
598
|
validate(cron) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const ok = await _cronValidator.isValidCron?.(cron, { alias: true, allowBlankDay: true });
|
|
208
|
-
this.error = ok
|
|
209
|
-
? null
|
|
210
|
-
: this.metadata.validators?.invalidCronMessage || 'Invalid CRON expression';
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// On import/validation failure, do a minimal sanity check and do not block UX
|
|
214
|
-
this.error = /\S+ \S+ \S+ \S+ \S+/.test(cron) ? null : 'Invalid CRON expression';
|
|
215
|
-
}
|
|
216
|
-
})();
|
|
599
|
+
this.error = isValidCronExpression(cron)
|
|
600
|
+
? null
|
|
601
|
+
: this.metadata.validators?.invalidCronMessage || 'Invalid CRON expression';
|
|
217
602
|
}
|
|
218
603
|
humanize(cron) {
|
|
219
604
|
if (this.error) {
|
|
220
605
|
this.humanized = '';
|
|
221
606
|
return;
|
|
222
607
|
}
|
|
223
|
-
|
|
224
|
-
try {
|
|
225
|
-
_cronstrue = _cronstrue || (await import('cronstrue'));
|
|
226
|
-
this.humanized = _cronstrue.toString?.(cron, { locale: this.metadata.locale }) || '';
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
this.humanized = '';
|
|
230
|
-
}
|
|
231
|
-
})();
|
|
608
|
+
this.humanized = humanizeCronExpression(cron, this.metadata.locale || 'en-US');
|
|
232
609
|
}
|
|
233
610
|
generatePreview(cron) {
|
|
234
611
|
if (this.error || !this.metadata.previewOccurrences) {
|
|
235
612
|
this.preview = [];
|
|
236
613
|
return;
|
|
237
614
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const interval = parse?.(cron, {
|
|
243
|
-
currentDate: this.metadata.previewFrom ?? new Date(),
|
|
244
|
-
tz: this.metadata.timezone,
|
|
245
|
-
});
|
|
246
|
-
const next = [];
|
|
247
|
-
for (let i = 0; i < (this.metadata.previewOccurrences ?? 5); i++) {
|
|
248
|
-
next.push(interval.next().toDate());
|
|
249
|
-
}
|
|
250
|
-
this.preview = next;
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
this.preview = [];
|
|
254
|
-
}
|
|
255
|
-
})();
|
|
615
|
+
this.preview = getNextCronOccurrences(cron, this.metadata.previewOccurrences ?? 5, {
|
|
616
|
+
currentDate: this.metadata.previewFrom ?? new Date(),
|
|
617
|
+
timeZone: this.metadata.timezone,
|
|
618
|
+
});
|
|
256
619
|
}
|
|
257
620
|
parseCronString(cron) {
|
|
258
621
|
const parts = cron.trim().split(/\s+/);
|
|
@@ -384,7 +747,7 @@ class PdxCronBuilderComponent {
|
|
|
384
747
|
useExisting: forwardRef(() => PdxCronBuilderComponent),
|
|
385
748
|
multi: true,
|
|
386
749
|
},
|
|
387
|
-
], ngImport: i0, template: "<div class=\"cron-builder-container\" (focusout)=\"onTouched()\">\n @if (metadata.mode === 'both') {\n <mat-tab-group\n [selectedIndex]=\"selectedTabIndex\"\n (selectedTabChange)=\"onTabChange($event)\"\n >\n <mat-tab label=\"Simple\"></mat-tab>\n <mat-tab label=\"Advanced\"></mat-tab>\n </mat-tab-group>\n }\n\n @if (value) {\n <div class=\"cron-expression\">\n <mat-form-field appearance=\"outline\" class=\"cron-expression-field\">\n <mat-label>CRON Expression</mat-label>\n <input matInput [value]=\"value\" readonly />\n <button\n mat-icon-button\n matSuffix\n (click)=\"copyCron()\"\n [matTooltip]=\"'Copy to clipboard'\"\n aria-label=\"Copy CRON expression\"\n >\n <mat-icon [praxisIcon]=\"'content_copy'\"></mat-icon>\n </button>\n </mat-form-field>\n <button mat-button (click)=\"importCron()\">Import CRON</button>\n </div>\n }\n\n @switch (activeTab) {\n @case ('simple') {\n <div [formGroup]=\"simpleForm\" class=\"simple-mode\">\n <mat-form-field appearance=\"outline\" class=\"preset-select\">\n <mat-label>Preset</mat-label>\n <mat-select formControlName=\"type\">\n <mat-option value=\"everyNMinutes\">A cada X min</mat-option>\n <mat-option value=\"dailyAt\">Diariamente \u00E0s</mat-option>\n <mat-option value=\"weekly\">Semanal (dias marcados) \u00E0s</mat-option>\n <mat-option value=\"monthlyDay\">Mensal (dia N) \u00E0s</mat-option>\n <mat-option value=\"monthlyNthWeekday\">\n Mensal (N-\u00E9sima 2\u00AA-feira) \u00E0s\n </mat-option>\n </mat-select>\n </mat-form-field>\n\n @switch (simpleControls.type.value) {\n @case ('everyNMinutes') {\n <div class=\"preset-body\">\n <mat-slider\n formControlName=\"everyN\"\n min=\"1\"\n max=\"60\"\n step=\"1\"\n thumbLabel\n ></mat-slider>\n <div class=\"cron-hint\">\n A cada {{ simpleControls.everyN.value }} minutos\n </div>\n </div>\n }\n\n @case ('dailyAt') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"dailyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Diariamente \u00E0s {{ simpleControls.dailyTime.value }}\n </div>\n </div>\n }\n\n @case ('weekly') {\n <div class=\"preset-body\">\n <mat-chip-listbox formControlName=\"weeklyDays\" multiple>\n @for (day of weeklyDayOptions; track day) {\n <mat-chip-option [value]=\"day\">\n {{ weekdayLabels[day] }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"weeklyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Semanalmente \u00E0s {{ simpleControls.weeklyTime.value }}\n </div>\n </div>\n }\n\n @case ('monthlyDay') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Dia</mat-label>\n <input\n matInput\n type=\"number\"\n formControlName=\"monthlyDay\"\n min=\"1\"\n max=\"31\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"monthlyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Dia {{ simpleControls.monthlyDay.value }} \u00E0s\n {{ simpleControls.monthlyTime.value }}\n </div>\n </div>\n }\n\n @case ('monthlyNthWeekday') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>N-\u00E9sima</mat-label>\n <mat-select formControlName=\"nth\">\n @for (nth of nthOrderOptions; track nth) {\n <mat-option [value]=\"nth\">{{ nth }}\u00AA</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-chip-listbox formControlName=\"nthDay\">\n @for (day of nthDayOptions; track day) {\n <mat-chip-option [value]=\"day\">\n {{ weekdayLabels[day] }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"nthTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n {{ simpleControls.nth.value }}\u00AA\n {{ weekdayLabels[simpleControls.nthDay.value] }}\n \u00E0s\n {{ simpleControls.nthTime.value }}\n </div>\n </div>\n }\n }\n </div>\n }\n\n @case ('advanced') {\n <div [formGroup]=\"form\">\n <div class=\"cron-fields\">\n @if (metadata.fields?.minutes) {\n <mat-form-field>\n <mat-label>Minutes</mat-label>\n <input matInput formControlName=\"minutes\" />\n </mat-form-field>\n }\n @if (metadata.fields?.hours) {\n <mat-form-field>\n <mat-label>Hours</mat-label>\n <input matInput formControlName=\"hours\" />\n </mat-form-field>\n }\n @if (metadata.fields?.dom) {\n <mat-form-field>\n <mat-label>Day of Month</mat-label>\n <input matInput formControlName=\"dayOfMonth\" />\n </mat-form-field>\n }\n @if (metadata.fields?.month) {\n <mat-form-field>\n <mat-label>Month</mat-label>\n <input matInput formControlName=\"month\" />\n </mat-form-field>\n }\n @if (metadata.fields?.dow) {\n <mat-form-field>\n <mat-label>Day of Week</mat-label>\n <input matInput formControlName=\"dayOfWeek\" />\n </mat-form-field>\n }\n @if (metadata.fields?.seconds) {\n <mat-form-field>\n <mat-label>Seconds</mat-label>\n <input matInput formControlName=\"seconds\" />\n </mat-form-field>\n }\n </div>\n </div>\n }\n }\n\n <div class=\"cron-feedback\">\n <mat-form-field appearance=\"outline\" class=\"timezone-field\">\n <mat-label>Timezone</mat-label>\n <mat-select [formControl]=\"timezoneControl\">\n @for (tz of timezoneOptions; track tz) {\n <mat-option [value]=\"tz\">\n {{ tz }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (humanized) {\n <div class=\"humanized-description\" aria-live=\"polite\">\n {{ humanized }}\n <button\n mat-icon-button\n class=\"copy-humanized\"\n (click)=\"copyHumanized()\"\n [matTooltip]=\"'Copy description'\"\n aria-label=\"Copy description\"\n >\n <mat-icon [praxisIcon]=\"'content_copy'\"></mat-icon>\n </button>\n </div>\n }\n\n @if (error) {\n <div class=\"cron-error\">{{ error }}</div>\n }\n\n @if (preview.length > 0) {\n <div class=\"preview-section\">\n <h4>Next Occurrences:</h4>\n <mat-list>\n @for (date of preview; track $index) {\n <mat-list-item>\n {{\n date\n | date\n : \"full\"\n : timezoneControl.value\n : metadata.locale || \"pt-BR\"\n }}\n </mat-list-item>\n }\n </mat-list>\n </div>\n }\n </div>\n\n @if (metadata.hint) {\n <div class=\"cron-hint\">{{ metadata.hint }}</div>\n }\n</div>\n", styles: [":host{display:block}.cron-expression-field{width:100%}.cron-expression{margin-bottom:1rem}.simple-mode{display:flex;flex-direction:column;gap:1rem}.preset-body{display:flex;flex-direction:column;gap:.5rem}.cron-feedback{margin-top:1rem;display:flex;flex-direction:column;gap:.5rem}.humanized-description{display:flex;align-items:center;gap:.5rem}.timezone-field{width:250px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i5.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i5.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatSliderModule }, { kind: "component", type: i9.MatSlider, selector: "mat-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "color", "disableRipple", "max", "step", "displayWith"], exportAs: ["matSlider"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i10.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i10.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "pipe", type: i11.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
750
|
+
], ngImport: i0, template: "<div class=\"cron-builder-container\" (focusout)=\"onTouched()\">\n @if (metadata.mode === 'both') {\n <mat-tab-group\n [selectedIndex]=\"selectedTabIndex\"\n (selectedTabChange)=\"onTabChange($event)\"\n >\n <mat-tab label=\"Simple\"></mat-tab>\n <mat-tab label=\"Advanced\"></mat-tab>\n </mat-tab-group>\n }\n\n @if (value) {\n <div class=\"cron-expression\">\n <mat-form-field appearance=\"outline\" class=\"cron-expression-field\">\n <mat-label>CRON Expression</mat-label>\n <input matInput [value]=\"value\" readonly />\n <button\n mat-icon-button\n matSuffix\n (click)=\"copyCron()\"\n [matTooltip]=\"'Copy to clipboard'\"\n aria-label=\"Copy CRON expression\"\n >\n <mat-icon [praxisIcon]=\"'content_copy'\"></mat-icon>\n </button>\n </mat-form-field>\n <button mat-button (click)=\"importCron()\">Import CRON</button>\n </div>\n }\n\n @switch (activeTab) {\n @case ('simple') {\n <div [formGroup]=\"simpleForm\" class=\"simple-mode\">\n <mat-form-field appearance=\"outline\" class=\"preset-select\">\n <mat-label>Preset</mat-label>\n <mat-select formControlName=\"type\">\n <mat-option value=\"everyNMinutes\">A cada X min</mat-option>\n <mat-option value=\"dailyAt\">Diariamente \u00E0s</mat-option>\n <mat-option value=\"weekly\">Semanal (dias marcados) \u00E0s</mat-option>\n <mat-option value=\"monthlyDay\">Mensal (dia N) \u00E0s</mat-option>\n <mat-option value=\"monthlyNthWeekday\">\n Mensal (N-\u00E9sima 2\u00AA-feira) \u00E0s\n </mat-option>\n </mat-select>\n </mat-form-field>\n\n @switch (simpleControls.type.value) {\n @case ('everyNMinutes') {\n <div class=\"preset-body\">\n <mat-slider\n min=\"1\"\n max=\"60\"\n step=\"1\"\n thumbLabel\n >\n <input matSliderThumb formControlName=\"everyN\" />\n </mat-slider>\n <div class=\"cron-hint\">\n A cada {{ simpleControls.everyN.value }} minutos\n </div>\n </div>\n }\n\n @case ('dailyAt') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"dailyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Diariamente \u00E0s {{ simpleControls.dailyTime.value }}\n </div>\n </div>\n }\n\n @case ('weekly') {\n <div class=\"preset-body\">\n <mat-chip-listbox formControlName=\"weeklyDays\" multiple>\n @for (day of weeklyDayOptions; track day) {\n <mat-chip-option [value]=\"day\">\n {{ weekdayLabels[day] }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"weeklyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Semanalmente \u00E0s {{ simpleControls.weeklyTime.value }}\n </div>\n </div>\n }\n\n @case ('monthlyDay') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Dia</mat-label>\n <input\n matInput\n type=\"number\"\n formControlName=\"monthlyDay\"\n min=\"1\"\n max=\"31\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"monthlyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Dia {{ simpleControls.monthlyDay.value }} \u00E0s\n {{ simpleControls.monthlyTime.value }}\n </div>\n </div>\n }\n\n @case ('monthlyNthWeekday') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>N-\u00E9sima</mat-label>\n <mat-select formControlName=\"nth\">\n @for (nth of nthOrderOptions; track nth) {\n <mat-option [value]=\"nth\">{{ nth }}\u00AA</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-chip-listbox formControlName=\"nthDay\">\n @for (day of nthDayOptions; track day) {\n <mat-chip-option [value]=\"day\">\n {{ weekdayLabels[day] }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"nthTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n {{ simpleControls.nth.value }}\u00AA\n {{ weekdayLabels[simpleControls.nthDay.value] }}\n \u00E0s\n {{ simpleControls.nthTime.value }}\n </div>\n </div>\n }\n }\n </div>\n }\n\n @case ('advanced') {\n <div [formGroup]=\"form\">\n <div class=\"cron-fields\">\n @if (metadata.fields?.minutes) {\n <mat-form-field>\n <mat-label>Minutes</mat-label>\n <input matInput formControlName=\"minutes\" />\n </mat-form-field>\n }\n @if (metadata.fields?.hours) {\n <mat-form-field>\n <mat-label>Hours</mat-label>\n <input matInput formControlName=\"hours\" />\n </mat-form-field>\n }\n @if (metadata.fields?.dom) {\n <mat-form-field>\n <mat-label>Day of Month</mat-label>\n <input matInput formControlName=\"dayOfMonth\" />\n </mat-form-field>\n }\n @if (metadata.fields?.month) {\n <mat-form-field>\n <mat-label>Month</mat-label>\n <input matInput formControlName=\"month\" />\n </mat-form-field>\n }\n @if (metadata.fields?.dow) {\n <mat-form-field>\n <mat-label>Day of Week</mat-label>\n <input matInput formControlName=\"dayOfWeek\" />\n </mat-form-field>\n }\n @if (metadata.fields?.seconds) {\n <mat-form-field>\n <mat-label>Seconds</mat-label>\n <input matInput formControlName=\"seconds\" />\n </mat-form-field>\n }\n </div>\n </div>\n }\n }\n\n <div class=\"cron-feedback\">\n <mat-form-field appearance=\"outline\" class=\"timezone-field\">\n <mat-label>Timezone</mat-label>\n <mat-select [formControl]=\"timezoneControl\">\n @for (tz of timezoneOptions; track tz) {\n <mat-option [value]=\"tz\">\n {{ tz }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (humanized) {\n <div class=\"humanized-description\" aria-live=\"polite\">\n {{ humanized }}\n <button\n mat-icon-button\n class=\"copy-humanized\"\n (click)=\"copyHumanized()\"\n [matTooltip]=\"'Copy description'\"\n aria-label=\"Copy description\"\n >\n <mat-icon [praxisIcon]=\"'content_copy'\"></mat-icon>\n </button>\n </div>\n }\n\n @if (error) {\n <div class=\"cron-error\">{{ error }}</div>\n }\n\n @if (preview.length > 0) {\n <div class=\"preview-section\">\n <h4>Next Occurrences:</h4>\n <mat-list>\n @for (date of preview; track $index) {\n <mat-list-item>\n {{\n date\n | date\n : \"full\"\n : timezoneControl.value\n : metadata.locale || \"pt-BR\"\n }}\n </mat-list-item>\n }\n </mat-list>\n </div>\n }\n </div>\n\n @if (metadata.hint) {\n <div class=\"cron-hint\">{{ metadata.hint }}</div>\n }\n</div>\n", styles: [":host{display:block}.cron-expression-field{width:100%}.cron-expression{margin-bottom:1rem}.simple-mode{display:flex;flex-direction:column;gap:1rem}.preset-body{display:flex;flex-direction:column;gap:.5rem}.cron-feedback{margin-top:1rem;display:flex;flex-direction:column;gap:.5rem}.humanized-description{display:flex;align-items:center;gap:.5rem}.timezone-field{width:250px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i2.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i2.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i3.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i3.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i5.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i5.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i8.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatSliderModule }, { kind: "component", type: i9.MatSlider, selector: "mat-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "color", "disableRipple", "max", "step", "displayWith"], exportAs: ["matSlider"] }, { kind: "directive", type: i9.MatSliderThumb, selector: "input[matSliderThumb]", inputs: ["value"], outputs: ["valueChange", "dragStart", "dragEnd"], exportAs: ["matSliderThumb"] }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i10.MatChipListbox, selector: "mat-chip-listbox", inputs: ["multiple", "aria-orientation", "selectable", "compareWith", "required", "hideSingleSelectionIndicator", "value"], outputs: ["change"] }, { kind: "component", type: i10.MatChipOption, selector: "mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]", inputs: ["selectable", "selected"], outputs: ["selectionChange"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "pipe", type: i11.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
388
751
|
}
|
|
389
752
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PdxCronBuilderComponent, decorators: [{
|
|
390
753
|
type: Component,
|
|
@@ -410,7 +773,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
410
773
|
useExisting: forwardRef(() => PdxCronBuilderComponent),
|
|
411
774
|
multi: true,
|
|
412
775
|
},
|
|
413
|
-
], template: "<div class=\"cron-builder-container\" (focusout)=\"onTouched()\">\n @if (metadata.mode === 'both') {\n <mat-tab-group\n [selectedIndex]=\"selectedTabIndex\"\n (selectedTabChange)=\"onTabChange($event)\"\n >\n <mat-tab label=\"Simple\"></mat-tab>\n <mat-tab label=\"Advanced\"></mat-tab>\n </mat-tab-group>\n }\n\n @if (value) {\n <div class=\"cron-expression\">\n <mat-form-field appearance=\"outline\" class=\"cron-expression-field\">\n <mat-label>CRON Expression</mat-label>\n <input matInput [value]=\"value\" readonly />\n <button\n mat-icon-button\n matSuffix\n (click)=\"copyCron()\"\n [matTooltip]=\"'Copy to clipboard'\"\n aria-label=\"Copy CRON expression\"\n >\n <mat-icon [praxisIcon]=\"'content_copy'\"></mat-icon>\n </button>\n </mat-form-field>\n <button mat-button (click)=\"importCron()\">Import CRON</button>\n </div>\n }\n\n @switch (activeTab) {\n @case ('simple') {\n <div [formGroup]=\"simpleForm\" class=\"simple-mode\">\n <mat-form-field appearance=\"outline\" class=\"preset-select\">\n <mat-label>Preset</mat-label>\n <mat-select formControlName=\"type\">\n <mat-option value=\"everyNMinutes\">A cada X min</mat-option>\n <mat-option value=\"dailyAt\">Diariamente \u00E0s</mat-option>\n <mat-option value=\"weekly\">Semanal (dias marcados) \u00E0s</mat-option>\n <mat-option value=\"monthlyDay\">Mensal (dia N) \u00E0s</mat-option>\n <mat-option value=\"monthlyNthWeekday\">\n Mensal (N-\u00E9sima 2\u00AA-feira) \u00E0s\n </mat-option>\n </mat-select>\n </mat-form-field>\n\n @switch (simpleControls.type.value) {\n @case ('everyNMinutes') {\n <div class=\"preset-body\">\n <mat-slider\n
|
|
776
|
+
], template: "<div class=\"cron-builder-container\" (focusout)=\"onTouched()\">\n @if (metadata.mode === 'both') {\n <mat-tab-group\n [selectedIndex]=\"selectedTabIndex\"\n (selectedTabChange)=\"onTabChange($event)\"\n >\n <mat-tab label=\"Simple\"></mat-tab>\n <mat-tab label=\"Advanced\"></mat-tab>\n </mat-tab-group>\n }\n\n @if (value) {\n <div class=\"cron-expression\">\n <mat-form-field appearance=\"outline\" class=\"cron-expression-field\">\n <mat-label>CRON Expression</mat-label>\n <input matInput [value]=\"value\" readonly />\n <button\n mat-icon-button\n matSuffix\n (click)=\"copyCron()\"\n [matTooltip]=\"'Copy to clipboard'\"\n aria-label=\"Copy CRON expression\"\n >\n <mat-icon [praxisIcon]=\"'content_copy'\"></mat-icon>\n </button>\n </mat-form-field>\n <button mat-button (click)=\"importCron()\">Import CRON</button>\n </div>\n }\n\n @switch (activeTab) {\n @case ('simple') {\n <div [formGroup]=\"simpleForm\" class=\"simple-mode\">\n <mat-form-field appearance=\"outline\" class=\"preset-select\">\n <mat-label>Preset</mat-label>\n <mat-select formControlName=\"type\">\n <mat-option value=\"everyNMinutes\">A cada X min</mat-option>\n <mat-option value=\"dailyAt\">Diariamente \u00E0s</mat-option>\n <mat-option value=\"weekly\">Semanal (dias marcados) \u00E0s</mat-option>\n <mat-option value=\"monthlyDay\">Mensal (dia N) \u00E0s</mat-option>\n <mat-option value=\"monthlyNthWeekday\">\n Mensal (N-\u00E9sima 2\u00AA-feira) \u00E0s\n </mat-option>\n </mat-select>\n </mat-form-field>\n\n @switch (simpleControls.type.value) {\n @case ('everyNMinutes') {\n <div class=\"preset-body\">\n <mat-slider\n min=\"1\"\n max=\"60\"\n step=\"1\"\n thumbLabel\n >\n <input matSliderThumb formControlName=\"everyN\" />\n </mat-slider>\n <div class=\"cron-hint\">\n A cada {{ simpleControls.everyN.value }} minutos\n </div>\n </div>\n }\n\n @case ('dailyAt') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"dailyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Diariamente \u00E0s {{ simpleControls.dailyTime.value }}\n </div>\n </div>\n }\n\n @case ('weekly') {\n <div class=\"preset-body\">\n <mat-chip-listbox formControlName=\"weeklyDays\" multiple>\n @for (day of weeklyDayOptions; track day) {\n <mat-chip-option [value]=\"day\">\n {{ weekdayLabels[day] }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"weeklyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Semanalmente \u00E0s {{ simpleControls.weeklyTime.value }}\n </div>\n </div>\n }\n\n @case ('monthlyDay') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>Dia</mat-label>\n <input\n matInput\n type=\"number\"\n formControlName=\"monthlyDay\"\n min=\"1\"\n max=\"31\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"monthlyTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n Dia {{ simpleControls.monthlyDay.value }} \u00E0s\n {{ simpleControls.monthlyTime.value }}\n </div>\n </div>\n }\n\n @case ('monthlyNthWeekday') {\n <div class=\"preset-body\">\n <mat-form-field appearance=\"outline\">\n <mat-label>N-\u00E9sima</mat-label>\n <mat-select formControlName=\"nth\">\n @for (nth of nthOrderOptions; track nth) {\n <mat-option [value]=\"nth\">{{ nth }}\u00AA</mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-chip-listbox formControlName=\"nthDay\">\n @for (day of nthDayOptions; track day) {\n <mat-chip-option [value]=\"day\">\n {{ weekdayLabels[day] }}\n </mat-chip-option>\n }\n </mat-chip-listbox>\n <mat-form-field appearance=\"outline\">\n <mat-label>Hora</mat-label>\n <input matInput type=\"time\" formControlName=\"nthTime\" />\n </mat-form-field>\n <div class=\"cron-hint\">\n {{ simpleControls.nth.value }}\u00AA\n {{ weekdayLabels[simpleControls.nthDay.value] }}\n \u00E0s\n {{ simpleControls.nthTime.value }}\n </div>\n </div>\n }\n }\n </div>\n }\n\n @case ('advanced') {\n <div [formGroup]=\"form\">\n <div class=\"cron-fields\">\n @if (metadata.fields?.minutes) {\n <mat-form-field>\n <mat-label>Minutes</mat-label>\n <input matInput formControlName=\"minutes\" />\n </mat-form-field>\n }\n @if (metadata.fields?.hours) {\n <mat-form-field>\n <mat-label>Hours</mat-label>\n <input matInput formControlName=\"hours\" />\n </mat-form-field>\n }\n @if (metadata.fields?.dom) {\n <mat-form-field>\n <mat-label>Day of Month</mat-label>\n <input matInput formControlName=\"dayOfMonth\" />\n </mat-form-field>\n }\n @if (metadata.fields?.month) {\n <mat-form-field>\n <mat-label>Month</mat-label>\n <input matInput formControlName=\"month\" />\n </mat-form-field>\n }\n @if (metadata.fields?.dow) {\n <mat-form-field>\n <mat-label>Day of Week</mat-label>\n <input matInput formControlName=\"dayOfWeek\" />\n </mat-form-field>\n }\n @if (metadata.fields?.seconds) {\n <mat-form-field>\n <mat-label>Seconds</mat-label>\n <input matInput formControlName=\"seconds\" />\n </mat-form-field>\n }\n </div>\n </div>\n }\n }\n\n <div class=\"cron-feedback\">\n <mat-form-field appearance=\"outline\" class=\"timezone-field\">\n <mat-label>Timezone</mat-label>\n <mat-select [formControl]=\"timezoneControl\">\n @for (tz of timezoneOptions; track tz) {\n <mat-option [value]=\"tz\">\n {{ tz }}\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n\n @if (humanized) {\n <div class=\"humanized-description\" aria-live=\"polite\">\n {{ humanized }}\n <button\n mat-icon-button\n class=\"copy-humanized\"\n (click)=\"copyHumanized()\"\n [matTooltip]=\"'Copy description'\"\n aria-label=\"Copy description\"\n >\n <mat-icon [praxisIcon]=\"'content_copy'\"></mat-icon>\n </button>\n </div>\n }\n\n @if (error) {\n <div class=\"cron-error\">{{ error }}</div>\n }\n\n @if (preview.length > 0) {\n <div class=\"preview-section\">\n <h4>Next Occurrences:</h4>\n <mat-list>\n @for (date of preview; track $index) {\n <mat-list-item>\n {{\n date\n | date\n : \"full\"\n : timezoneControl.value\n : metadata.locale || \"pt-BR\"\n }}\n </mat-list-item>\n }\n </mat-list>\n </div>\n }\n </div>\n\n @if (metadata.hint) {\n <div class=\"cron-hint\">{{ metadata.hint }}</div>\n }\n</div>\n", styles: [":host{display:block}.cron-expression-field{width:100%}.cron-expression{margin-bottom:1rem}.simple-mode{display:flex;flex-direction:column;gap:1rem}.preset-body{display:flex;flex-direction:column;gap:.5rem}.cron-feedback{margin-top:1rem;display:flex;flex-direction:column;gap:.5rem}.humanized-description{display:flex;align-items:center;gap:.5rem}.timezone-field{width:250px}\n"] }]
|
|
414
777
|
}], ctorParameters: () => [], propDecorators: { metadata: [{
|
|
415
778
|
type: Input
|
|
416
779
|
}] } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisui/cron-builder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-beta.0",
|
|
4
4
|
"description": "Cron expression builder utilities and components for Praxis UI.",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@angular/common": "^20.1.0",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"@angular/forms": "^20.1.0",
|
|
9
9
|
"@angular/cdk": "^20.1.0",
|
|
10
10
|
"@angular/material": "^20.1.0",
|
|
11
|
-
"@praxisui/core": "^
|
|
11
|
+
"@praxisui/core": "^8.0.0-beta.0"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"tslib": "^2.3.0",
|