@praxisui/cron-builder 8.0.0-beta.0 → 8.0.0-beta.100
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 +61 -0
- package/fesm2022/praxisui-cron-builder.mjs +802 -29
- package/package.json +13 -10
- package/types/praxisui-cron-builder.d.ts +288 -0
- package/index.d.ts +0 -137
|
@@ -429,6 +429,451 @@ function getNextCronOccurrences(expression, count, options) {
|
|
|
429
429
|
return results;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
const SCHEDULE_AUTHORING_CONFIG_VERSION = 'v1';
|
|
433
|
+
const CRON_DIALECTS = {
|
|
434
|
+
unix: {
|
|
435
|
+
dialect: 'unix',
|
|
436
|
+
label: 'Unix cron',
|
|
437
|
+
fieldCount: { min: 5, max: 5 },
|
|
438
|
+
supportsSeconds: false,
|
|
439
|
+
supportsYear: false,
|
|
440
|
+
supportsNamedMonths: true,
|
|
441
|
+
supportsNamedWeekdays: true,
|
|
442
|
+
supportsQuestionMark: false,
|
|
443
|
+
supportsLast: false,
|
|
444
|
+
supportsNearestWeekday: false,
|
|
445
|
+
supportsNthWeekday: false,
|
|
446
|
+
timezoneMode: 'host-local',
|
|
447
|
+
dayOfMonthDayOfWeekRule: 'unix-or',
|
|
448
|
+
examples: ['*/5 * * * *', '0 9 * * 1-5'],
|
|
449
|
+
},
|
|
450
|
+
quartz: {
|
|
451
|
+
dialect: 'quartz',
|
|
452
|
+
label: 'Quartz cron',
|
|
453
|
+
fieldCount: { min: 6, max: 7 },
|
|
454
|
+
supportsSeconds: true,
|
|
455
|
+
supportsYear: true,
|
|
456
|
+
supportsNamedMonths: true,
|
|
457
|
+
supportsNamedWeekdays: true,
|
|
458
|
+
supportsQuestionMark: true,
|
|
459
|
+
supportsLast: true,
|
|
460
|
+
supportsNearestWeekday: true,
|
|
461
|
+
supportsNthWeekday: true,
|
|
462
|
+
timezoneMode: 'external-field',
|
|
463
|
+
dayOfMonthDayOfWeekRule: 'either-or-question-mark',
|
|
464
|
+
examples: ['0 0 9 ? * MON-FRI', '0 0 9 ? * 2#1'],
|
|
465
|
+
},
|
|
466
|
+
'aws-eventbridge': {
|
|
467
|
+
dialect: 'aws-eventbridge',
|
|
468
|
+
label: 'AWS EventBridge cron',
|
|
469
|
+
fieldCount: { min: 6, max: 6 },
|
|
470
|
+
supportsSeconds: false,
|
|
471
|
+
supportsYear: true,
|
|
472
|
+
supportsNamedMonths: true,
|
|
473
|
+
supportsNamedWeekdays: true,
|
|
474
|
+
supportsQuestionMark: true,
|
|
475
|
+
supportsLast: true,
|
|
476
|
+
supportsNearestWeekday: false,
|
|
477
|
+
supportsNthWeekday: true,
|
|
478
|
+
timezoneMode: 'external-field',
|
|
479
|
+
dayOfMonthDayOfWeekRule: 'either-or-question-mark',
|
|
480
|
+
examples: ['0 9 ? * MON-FRI *', '0 9 ? * 2#1 *'],
|
|
481
|
+
},
|
|
482
|
+
kubernetes: {
|
|
483
|
+
dialect: 'kubernetes',
|
|
484
|
+
label: 'Kubernetes CronJob',
|
|
485
|
+
fieldCount: { min: 5, max: 5 },
|
|
486
|
+
supportsSeconds: false,
|
|
487
|
+
supportsYear: false,
|
|
488
|
+
supportsNamedMonths: true,
|
|
489
|
+
supportsNamedWeekdays: true,
|
|
490
|
+
supportsQuestionMark: true,
|
|
491
|
+
supportsLast: false,
|
|
492
|
+
supportsNearestWeekday: false,
|
|
493
|
+
supportsNthWeekday: false,
|
|
494
|
+
timezoneMode: 'external-field',
|
|
495
|
+
dayOfMonthDayOfWeekRule: 'unix-or',
|
|
496
|
+
examples: ['*/5 * * * *', '0 9 * * 1-5'],
|
|
497
|
+
},
|
|
498
|
+
'github-actions': {
|
|
499
|
+
dialect: 'github-actions',
|
|
500
|
+
label: 'GitHub Actions schedule',
|
|
501
|
+
fieldCount: { min: 5, max: 5 },
|
|
502
|
+
supportsSeconds: false,
|
|
503
|
+
supportsYear: false,
|
|
504
|
+
supportsNamedMonths: true,
|
|
505
|
+
supportsNamedWeekdays: true,
|
|
506
|
+
supportsQuestionMark: false,
|
|
507
|
+
supportsLast: false,
|
|
508
|
+
supportsNearestWeekday: false,
|
|
509
|
+
supportsNthWeekday: false,
|
|
510
|
+
timezoneMode: 'fixed-utc',
|
|
511
|
+
dayOfMonthDayOfWeekRule: 'unix-or',
|
|
512
|
+
examples: ['*/15 * * * *', '0 9 * * 1-5'],
|
|
513
|
+
},
|
|
514
|
+
'gcp-scheduler': {
|
|
515
|
+
dialect: 'gcp-scheduler',
|
|
516
|
+
label: 'Google Cloud Scheduler',
|
|
517
|
+
fieldCount: { min: 5, max: 5 },
|
|
518
|
+
supportsSeconds: false,
|
|
519
|
+
supportsYear: false,
|
|
520
|
+
supportsNamedMonths: true,
|
|
521
|
+
supportsNamedWeekdays: true,
|
|
522
|
+
supportsQuestionMark: false,
|
|
523
|
+
supportsLast: false,
|
|
524
|
+
supportsNearestWeekday: false,
|
|
525
|
+
supportsNthWeekday: false,
|
|
526
|
+
timezoneMode: 'external-field',
|
|
527
|
+
dayOfMonthDayOfWeekRule: 'unix-or',
|
|
528
|
+
examples: ['*/5 * * * *', '0 9 * * MON-FRI'],
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
function getCronDialectDefinition(dialect) {
|
|
532
|
+
return CRON_DIALECTS[dialect];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function normalizeScheduleValue(value, options = {}) {
|
|
536
|
+
if (value == null || value === '') {
|
|
537
|
+
return {
|
|
538
|
+
config: null,
|
|
539
|
+
diagnostics: [],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
if (typeof value === 'string') {
|
|
543
|
+
return normalizeCronString(value, options);
|
|
544
|
+
}
|
|
545
|
+
return normalizeScheduleConfig(value, options);
|
|
546
|
+
}
|
|
547
|
+
function inferCronDialect(expression) {
|
|
548
|
+
const fieldCount = expression.trim().split(/\s+/).filter(Boolean).length;
|
|
549
|
+
return fieldCount === 6 ? 'quartz' : 'unix';
|
|
550
|
+
}
|
|
551
|
+
function normalizeCronString(cron, options) {
|
|
552
|
+
const trimmed = cron.trim();
|
|
553
|
+
const dialect = options.dialect ?? inferCronDialect(trimmed);
|
|
554
|
+
const diagnostics = createCronDiagnostics(trimmed, dialect);
|
|
555
|
+
return {
|
|
556
|
+
config: {
|
|
557
|
+
version: SCHEDULE_AUTHORING_CONFIG_VERSION,
|
|
558
|
+
kind: 'customCron',
|
|
559
|
+
timezone: options.timezone ?? 'UTC',
|
|
560
|
+
locale: options.locale,
|
|
561
|
+
expression: {
|
|
562
|
+
cron: trimmed,
|
|
563
|
+
dialect,
|
|
564
|
+
seconds: trimmed.split(/\s+/).length === 6 && dialect !== 'aws-eventbridge',
|
|
565
|
+
},
|
|
566
|
+
preview: {
|
|
567
|
+
occurrences: options.previewOccurrences ?? 5,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
diagnostics,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function normalizeScheduleConfig(config, options) {
|
|
574
|
+
const normalized = {
|
|
575
|
+
...config,
|
|
576
|
+
version: SCHEDULE_AUTHORING_CONFIG_VERSION,
|
|
577
|
+
timezone: config.timezone || options.timezone || 'UTC',
|
|
578
|
+
locale: config.locale ?? options.locale,
|
|
579
|
+
preview: {
|
|
580
|
+
occurrences: options.previewOccurrences ?? config.preview?.occurrences ?? 5,
|
|
581
|
+
from: config.preview?.from,
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
return {
|
|
585
|
+
config: normalized,
|
|
586
|
+
diagnostics: normalized.expression?.cron
|
|
587
|
+
? createCronDiagnostics(normalized.expression.cron, normalized.expression.dialect)
|
|
588
|
+
: [],
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function createCronDiagnostics(cron, dialect) {
|
|
592
|
+
const diagnostics = [];
|
|
593
|
+
const fieldCount = cron.trim().split(/\s+/).filter(Boolean).length;
|
|
594
|
+
if (dialect === 'aws-eventbridge' && fieldCount !== 6) {
|
|
595
|
+
diagnostics.push({
|
|
596
|
+
severity: 'error',
|
|
597
|
+
code: 'cron.aws.fieldCount',
|
|
598
|
+
field: 'expression.cron',
|
|
599
|
+
message: 'AWS EventBridge schedules require six fields.',
|
|
600
|
+
suggestion: 'Use the AWS order: minutes hours day-of-month month day-of-week year.',
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
else if (dialect !== 'aws-eventbridge' && !isValidCronExpression(cron)) {
|
|
604
|
+
diagnostics.push({
|
|
605
|
+
severity: 'error',
|
|
606
|
+
code: 'cron.invalid',
|
|
607
|
+
field: 'expression.cron',
|
|
608
|
+
message: 'Invalid CRON expression for the selected dialect.',
|
|
609
|
+
suggestion: 'Check field count, ranges, steps and unsupported special characters.',
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (dialect === 'unix' && fieldCount !== 5) {
|
|
613
|
+
diagnostics.push({
|
|
614
|
+
severity: 'error',
|
|
615
|
+
code: 'cron.unix.fieldCount',
|
|
616
|
+
field: 'expression.cron',
|
|
617
|
+
message: 'Unix cron schedules require five fields.',
|
|
618
|
+
suggestion: 'Remove seconds or year fields, or select a dialect that supports them.',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (dialect === 'quartz' && fieldCount < 6) {
|
|
622
|
+
diagnostics.push({
|
|
623
|
+
severity: 'warning',
|
|
624
|
+
code: 'cron.quartz.fieldCount',
|
|
625
|
+
field: 'expression.cron',
|
|
626
|
+
message: 'Quartz cron commonly uses seconds as the first field.',
|
|
627
|
+
suggestion: 'Add a seconds field or select the Unix dialect.',
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
return diagnostics;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function compileScheduleExpression(config) {
|
|
634
|
+
const diagnostics = [];
|
|
635
|
+
const dialect = config.expression?.dialect ?? 'unix';
|
|
636
|
+
if (config.kind === 'customCron') {
|
|
637
|
+
if (!config.expression?.cron) {
|
|
638
|
+
diagnostics.push(createDiagnostic('error', 'schedule.cron.required', 'expression.cron', 'A CRON expression is required for custom schedules.'));
|
|
639
|
+
return { expression: null, diagnostics };
|
|
640
|
+
}
|
|
641
|
+
const seconds = config.expression.seconds ?? config.expression.cron.trim().split(/\s+/).length === 6;
|
|
642
|
+
return {
|
|
643
|
+
expression: {
|
|
644
|
+
cron: config.expression.cron.trim(),
|
|
645
|
+
dialect,
|
|
646
|
+
seconds,
|
|
647
|
+
},
|
|
648
|
+
diagnostics,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (config.kind === 'once') {
|
|
652
|
+
diagnostics.push(createDiagnostic('warning', 'schedule.once.noCron', 'kind', 'One-time schedules do not have a portable CRON representation.'));
|
|
653
|
+
return { expression: null, diagnostics };
|
|
654
|
+
}
|
|
655
|
+
if (config.kind === 'interval') {
|
|
656
|
+
return compileIntervalSchedule(config, dialect);
|
|
657
|
+
}
|
|
658
|
+
if (config.kind === 'daily') {
|
|
659
|
+
return compileDailySchedule(config, dialect);
|
|
660
|
+
}
|
|
661
|
+
if (config.kind === 'weekly') {
|
|
662
|
+
return compileWeeklySchedule(config, dialect);
|
|
663
|
+
}
|
|
664
|
+
if (config.kind === 'monthly') {
|
|
665
|
+
return compileMonthlySchedule(config, dialect);
|
|
666
|
+
}
|
|
667
|
+
diagnostics.push(createDiagnostic('error', 'schedule.kind.unsupported', 'kind', 'Unsupported schedule kind.'));
|
|
668
|
+
return { expression: null, diagnostics };
|
|
669
|
+
}
|
|
670
|
+
function validateScheduleAuthoringConfig(config) {
|
|
671
|
+
const compiled = compileScheduleExpression(config);
|
|
672
|
+
const diagnostics = [...compiled.diagnostics];
|
|
673
|
+
if (compiled.expression && compiled.expression.dialect !== 'aws-eventbridge' && !isValidCronExpression(compiled.expression.cron)) {
|
|
674
|
+
diagnostics.push(createDiagnostic('error', 'schedule.cron.invalid', 'expression.cron', 'The compiled CRON expression is invalid.'));
|
|
675
|
+
}
|
|
676
|
+
if (!config.timezone) {
|
|
677
|
+
diagnostics.push(createDiagnostic('warning', 'schedule.timezone.defaulted', 'timezone', 'Timezone is missing and will default to UTC.'));
|
|
678
|
+
}
|
|
679
|
+
if (config.window?.startAt && config.window?.endAt && Date.parse(config.window.startAt) > Date.parse(config.window.endAt)) {
|
|
680
|
+
diagnostics.push(createDiagnostic('error', 'schedule.window.invalid', 'window', 'The schedule start date must be before the end date.'));
|
|
681
|
+
}
|
|
682
|
+
return diagnostics;
|
|
683
|
+
}
|
|
684
|
+
function createSchedulePreview(value, preview = {}) {
|
|
685
|
+
const normalized = normalizeScheduleValue(value, {
|
|
686
|
+
timezone: preview.timezone,
|
|
687
|
+
locale: preview.locale,
|
|
688
|
+
dialect: preview.dialect,
|
|
689
|
+
previewOccurrences: preview.occurrences,
|
|
690
|
+
});
|
|
691
|
+
const config = normalized.config;
|
|
692
|
+
if (!config) {
|
|
693
|
+
return {
|
|
694
|
+
config: null,
|
|
695
|
+
expression: null,
|
|
696
|
+
humanized: '',
|
|
697
|
+
occurrences: [],
|
|
698
|
+
diagnostics: normalized.diagnostics,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
const compiled = compileScheduleExpression(config);
|
|
702
|
+
const diagnostics = mergeDiagnostics([
|
|
703
|
+
...normalized.diagnostics,
|
|
704
|
+
...validateScheduleAuthoringConfig(config),
|
|
705
|
+
]);
|
|
706
|
+
if (!compiled.expression || diagnostics.some((diagnostic) => diagnostic.severity === 'error')) {
|
|
707
|
+
return {
|
|
708
|
+
config,
|
|
709
|
+
expression: compiled.expression,
|
|
710
|
+
humanized: '',
|
|
711
|
+
occurrences: [],
|
|
712
|
+
diagnostics,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const from = preview.from ?? config.preview?.from ?? config.window?.startAt;
|
|
716
|
+
const count = preview.occurrences ?? config.preview?.occurrences ?? 5;
|
|
717
|
+
const currentDate = from ? new Date(from) : new Date();
|
|
718
|
+
const timezone = config.timezone || 'UTC';
|
|
719
|
+
const occurrences = getNextCronOccurrences(compiled.expression.cron, count, {
|
|
720
|
+
currentDate,
|
|
721
|
+
timeZone: timezone,
|
|
722
|
+
}).map((date) => ({
|
|
723
|
+
instant: date.toISOString(),
|
|
724
|
+
localDateTime: formatLocalDateTime(date, timezone, config.locale ?? 'en-US'),
|
|
725
|
+
timezone,
|
|
726
|
+
}));
|
|
727
|
+
return {
|
|
728
|
+
config,
|
|
729
|
+
expression: compiled.expression,
|
|
730
|
+
humanized: humanizeCronExpression(compiled.expression.cron, config.locale ?? 'en-US'),
|
|
731
|
+
occurrences,
|
|
732
|
+
diagnostics,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function compileIntervalSchedule(config, dialect) {
|
|
736
|
+
const interval = config.recurrence?.interval;
|
|
737
|
+
const diagnostics = [];
|
|
738
|
+
if (!interval || interval.every < 1) {
|
|
739
|
+
diagnostics.push(createDiagnostic('error', 'schedule.interval.invalid', 'recurrence.interval', 'Interval schedules require a positive interval.'));
|
|
740
|
+
return { expression: null, diagnostics };
|
|
741
|
+
}
|
|
742
|
+
if (interval.unit === 'minutes') {
|
|
743
|
+
if (interval.every > 59) {
|
|
744
|
+
diagnostics.push(createDiagnostic('error', 'schedule.interval.minutes.range', 'recurrence.interval.every', 'Minute intervals must be between 1 and 59.'));
|
|
745
|
+
return { expression: null, diagnostics };
|
|
746
|
+
}
|
|
747
|
+
return makeExpression(`*/${interval.every} * * * *`, dialect, false, diagnostics);
|
|
748
|
+
}
|
|
749
|
+
if (interval.unit === 'hours') {
|
|
750
|
+
if (interval.every > 23) {
|
|
751
|
+
diagnostics.push(createDiagnostic('error', 'schedule.interval.hours.range', 'recurrence.interval.every', 'Hourly intervals must be between 1 and 23.'));
|
|
752
|
+
return { expression: null, diagnostics };
|
|
753
|
+
}
|
|
754
|
+
return makeExpression(`0 */${interval.every} * * *`, dialect, false, diagnostics);
|
|
755
|
+
}
|
|
756
|
+
diagnostics.push(createDiagnostic('warning', 'schedule.interval.notPortable', 'recurrence.interval.unit', 'Day and week intervals are not portable CRON schedules without an anchor date.'));
|
|
757
|
+
return { expression: null, diagnostics };
|
|
758
|
+
}
|
|
759
|
+
function compileDailySchedule(config, dialect) {
|
|
760
|
+
const daily = config.recurrence?.daily;
|
|
761
|
+
const diagnostics = validateTimes(daily?.times, 'recurrence.daily.times');
|
|
762
|
+
if (!daily || diagnostics.length > 0) {
|
|
763
|
+
return { expression: null, diagnostics };
|
|
764
|
+
}
|
|
765
|
+
const time = parseTime(daily.times[0]);
|
|
766
|
+
const dayOfWeek = daily.onlyWeekdays ? '1-5' : '*';
|
|
767
|
+
return makeExpression(`${time.minute} ${time.hour} * * ${dayOfWeek}`, dialect, false, diagnostics);
|
|
768
|
+
}
|
|
769
|
+
function compileWeeklySchedule(config, dialect) {
|
|
770
|
+
const weekly = config.recurrence?.weekly;
|
|
771
|
+
const diagnostics = validateTimes(weekly?.times, 'recurrence.weekly.times');
|
|
772
|
+
if (!weekly || diagnostics.length > 0) {
|
|
773
|
+
return { expression: null, diagnostics };
|
|
774
|
+
}
|
|
775
|
+
if (!weekly.daysOfWeek?.length) {
|
|
776
|
+
diagnostics.push(createDiagnostic('error', 'schedule.weekly.days.required', 'recurrence.weekly.daysOfWeek', 'Weekly schedules require at least one weekday.'));
|
|
777
|
+
return { expression: null, diagnostics };
|
|
778
|
+
}
|
|
779
|
+
const time = parseTime(weekly.times[0]);
|
|
780
|
+
const days = normalizeWeekdays(weekly.daysOfWeek).join(',');
|
|
781
|
+
return makeExpression(`${time.minute} ${time.hour} * * ${days}`, dialect, false, diagnostics);
|
|
782
|
+
}
|
|
783
|
+
function compileMonthlySchedule(config, dialect) {
|
|
784
|
+
const monthly = config.recurrence?.monthly;
|
|
785
|
+
const diagnostics = validateTimes(monthly?.times, 'recurrence.monthly.times');
|
|
786
|
+
if (!monthly || diagnostics.length > 0) {
|
|
787
|
+
return { expression: null, diagnostics };
|
|
788
|
+
}
|
|
789
|
+
const time = parseTime(monthly.times[0]);
|
|
790
|
+
if (monthly.mode === 'dayOfMonth') {
|
|
791
|
+
if (!monthly.dayOfMonth || monthly.dayOfMonth < 1 || monthly.dayOfMonth > 31) {
|
|
792
|
+
diagnostics.push(createDiagnostic('error', 'schedule.monthly.day.range', 'recurrence.monthly.dayOfMonth', 'Monthly day must be between 1 and 31.'));
|
|
793
|
+
return { expression: null, diagnostics };
|
|
794
|
+
}
|
|
795
|
+
return makeExpression(`${time.minute} ${time.hour} ${monthly.dayOfMonth} * *`, dialect, false, diagnostics);
|
|
796
|
+
}
|
|
797
|
+
if (monthly.mode === 'nthWeekday') {
|
|
798
|
+
if (!monthly.nth || monthly.weekday == null) {
|
|
799
|
+
diagnostics.push(createDiagnostic('error', 'schedule.monthly.nth.required', 'recurrence.monthly', 'Nth weekday schedules require nth and weekday.'));
|
|
800
|
+
return { expression: null, diagnostics };
|
|
801
|
+
}
|
|
802
|
+
return makeExpression(`${time.minute} ${time.hour} ? * ${monthly.weekday}#${monthly.nth}`, dialect, false, diagnostics);
|
|
803
|
+
}
|
|
804
|
+
diagnostics.push(createDiagnostic('warning', 'schedule.monthly.mode.notPortable', 'recurrence.monthly.mode', 'This monthly mode needs a dialect-specific compiler before it can produce CRON.'));
|
|
805
|
+
return { expression: null, diagnostics };
|
|
806
|
+
}
|
|
807
|
+
function makeExpression(cron, dialect, seconds, diagnostics) {
|
|
808
|
+
if (dialect === 'quartz') {
|
|
809
|
+
return {
|
|
810
|
+
expression: {
|
|
811
|
+
cron: seconds ? cron : `0 ${cron}`,
|
|
812
|
+
dialect,
|
|
813
|
+
seconds: true,
|
|
814
|
+
},
|
|
815
|
+
diagnostics,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
expression: {
|
|
820
|
+
cron,
|
|
821
|
+
dialect,
|
|
822
|
+
seconds,
|
|
823
|
+
},
|
|
824
|
+
diagnostics,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function validateTimes(times, field) {
|
|
828
|
+
if (!times?.length) {
|
|
829
|
+
return [createDiagnostic('error', 'schedule.time.required', field, 'At least one time is required.')];
|
|
830
|
+
}
|
|
831
|
+
if (times.length > 1) {
|
|
832
|
+
return [createDiagnostic('warning', 'schedule.time.multiple.unsupported', field, 'Only the first time is compiled in this initial scheduler runtime.')];
|
|
833
|
+
}
|
|
834
|
+
const time = parseTime(times[0]);
|
|
835
|
+
if (time.hour < 0 || time.hour > 23 || time.minute < 0 || time.minute > 59) {
|
|
836
|
+
return [createDiagnostic('error', 'schedule.time.invalid', field, 'Time must use HH:mm with a 24-hour clock.')];
|
|
837
|
+
}
|
|
838
|
+
return [];
|
|
839
|
+
}
|
|
840
|
+
function parseTime(value) {
|
|
841
|
+
if (!/^\d{2}:\d{2}$/.test(value)) {
|
|
842
|
+
return { hour: Number.NaN, minute: Number.NaN };
|
|
843
|
+
}
|
|
844
|
+
const [hour, minute] = value.split(':').map((part) => Number(part));
|
|
845
|
+
return { hour, minute };
|
|
846
|
+
}
|
|
847
|
+
function normalizeWeekdays(days) {
|
|
848
|
+
return Array.from(new Set(days)).sort((a, b) => a - b);
|
|
849
|
+
}
|
|
850
|
+
function formatLocalDateTime(date, timezone, locale) {
|
|
851
|
+
return new Intl.DateTimeFormat(locale, {
|
|
852
|
+
timeZone: timezone,
|
|
853
|
+
dateStyle: 'full',
|
|
854
|
+
timeStyle: 'long',
|
|
855
|
+
}).format(date);
|
|
856
|
+
}
|
|
857
|
+
function mergeDiagnostics(diagnostics) {
|
|
858
|
+
const seen = new Set();
|
|
859
|
+
return diagnostics.filter((diagnostic) => {
|
|
860
|
+
const key = `${diagnostic.severity}:${diagnostic.code}:${diagnostic.field ?? ''}:${diagnostic.message}`;
|
|
861
|
+
if (seen.has(key)) {
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
seen.add(key);
|
|
865
|
+
return true;
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function createDiagnostic(severity, code, field, message) {
|
|
869
|
+
return {
|
|
870
|
+
severity,
|
|
871
|
+
code,
|
|
872
|
+
field,
|
|
873
|
+
message,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
432
877
|
class PdxCronBuilderComponent {
|
|
433
878
|
// Tipos auxiliares para Typed Forms
|
|
434
879
|
fb = inject(NonNullableFormBuilder);
|
|
@@ -462,6 +907,9 @@ class PdxCronBuilderComponent {
|
|
|
462
907
|
destroy$ = new Subject();
|
|
463
908
|
humanized = '';
|
|
464
909
|
preview = [];
|
|
910
|
+
structuredPreview = [];
|
|
911
|
+
scheduleConfig = null;
|
|
912
|
+
scheduleDiagnostics = [];
|
|
465
913
|
error = null;
|
|
466
914
|
// Opções expostas ao template para controle de fluxo moderno
|
|
467
915
|
weekdayLabels = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
|
|
@@ -522,7 +970,7 @@ class PdxCronBuilderComponent {
|
|
|
522
970
|
.subscribe((tz) => {
|
|
523
971
|
this.metadata.timezone = tz || 'UTC';
|
|
524
972
|
if (typeof this.value === 'string') {
|
|
525
|
-
this.
|
|
973
|
+
this.refreshScheduleState(this.value);
|
|
526
974
|
}
|
|
527
975
|
});
|
|
528
976
|
}
|
|
@@ -569,9 +1017,7 @@ class PdxCronBuilderComponent {
|
|
|
569
1017
|
if (emitEvent) {
|
|
570
1018
|
this.onChange(cron);
|
|
571
1019
|
}
|
|
572
|
-
this.
|
|
573
|
-
this.humanize(cron);
|
|
574
|
-
this.generatePreview(cron);
|
|
1020
|
+
this.refreshScheduleState(cron);
|
|
575
1021
|
}
|
|
576
1022
|
copyCron() {
|
|
577
1023
|
if (typeof this.value === 'string') {
|
|
@@ -595,27 +1041,22 @@ class PdxCronBuilderComponent {
|
|
|
595
1041
|
this.syncSimpleForm(cron);
|
|
596
1042
|
}
|
|
597
1043
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
: this.metadata.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (this.error) {
|
|
605
|
-
this.humanized = '';
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
this.humanized = humanizeCronExpression(cron, this.metadata.locale || 'en-US');
|
|
609
|
-
}
|
|
610
|
-
generatePreview(cron) {
|
|
611
|
-
if (this.error || !this.metadata.previewOccurrences) {
|
|
612
|
-
this.preview = [];
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
this.preview = getNextCronOccurrences(cron, this.metadata.previewOccurrences ?? 5, {
|
|
616
|
-
currentDate: this.metadata.previewFrom ?? new Date(),
|
|
617
|
-
timeZone: this.metadata.timezone,
|
|
1044
|
+
refreshScheduleState(cron) {
|
|
1045
|
+
const preview = createSchedulePreview(cron, {
|
|
1046
|
+
occurrences: this.metadata.previewOccurrences ?? 5,
|
|
1047
|
+
from: this.metadata.previewFrom?.toISOString(),
|
|
1048
|
+
timezone: this.metadata.timezone || this.timezoneControl.value || 'UTC',
|
|
1049
|
+
locale: this.metadata.locale || 'en-US',
|
|
618
1050
|
});
|
|
1051
|
+
this.scheduleConfig = preview.config;
|
|
1052
|
+
this.scheduleDiagnostics = preview.diagnostics;
|
|
1053
|
+
this.structuredPreview = preview.occurrences;
|
|
1054
|
+
this.humanized = preview.humanized;
|
|
1055
|
+
this.preview = preview.occurrences.map((occurrence) => new Date(occurrence.instant));
|
|
1056
|
+
const error = preview.diagnostics.find((diagnostic) => diagnostic.severity === 'error');
|
|
1057
|
+
this.error = error
|
|
1058
|
+
? this.metadata.validators?.invalidCronMessage || error.message || 'Invalid CRON expression'
|
|
1059
|
+
: null;
|
|
619
1060
|
}
|
|
620
1061
|
parseCronString(cron) {
|
|
621
1062
|
const parts = cron.trim().split(/\s+/);
|
|
@@ -740,16 +1181,16 @@ class PdxCronBuilderComponent {
|
|
|
740
1181
|
this.destroy$.next();
|
|
741
1182
|
this.destroy$.complete();
|
|
742
1183
|
}
|
|
743
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
744
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "
|
|
1184
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PdxCronBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1185
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PdxCronBuilderComponent, isStandalone: true, selector: "pdx-cron-builder", inputs: { metadata: "metadata" }, providers: [
|
|
745
1186
|
{
|
|
746
1187
|
provide: NG_VALUE_ACCESSOR,
|
|
747
1188
|
useExisting: forwardRef(() => PdxCronBuilderComponent),
|
|
748
1189
|
multi: true,
|
|
749
1190
|
},
|
|
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 });
|
|
1191
|
+
], 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],[formArray],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 });
|
|
751
1192
|
}
|
|
752
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1193
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PdxCronBuilderComponent, decorators: [{
|
|
753
1194
|
type: Component,
|
|
754
1195
|
args: [{ selector: 'pdx-cron-builder', standalone: true, imports: [
|
|
755
1196
|
CommonModule,
|
|
@@ -806,6 +1247,12 @@ const PDX_CRON_BUILDER_DOC_META_LEGACY = {
|
|
|
806
1247
|
*/
|
|
807
1248
|
const ENUMS = {
|
|
808
1249
|
mode: ['simple', 'advanced', 'both'],
|
|
1250
|
+
scheduleKind: ['once', 'interval', 'daily', 'weekly', 'monthly', 'customCron'],
|
|
1251
|
+
cronDialect: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'],
|
|
1252
|
+
intervalUnit: ['minutes', 'hours', 'days', 'weeks'],
|
|
1253
|
+
monthlyMode: ['dayOfMonth', 'nthWeekday', 'lastDay', 'lastWeekday'],
|
|
1254
|
+
misfirePolicy: ['skip', 'run-late', 'run-once'],
|
|
1255
|
+
concurrencyPolicy: ['allow', 'forbid', 'replace'],
|
|
809
1256
|
validationTrigger: ['change', 'blur'],
|
|
810
1257
|
errorPosition: ['tooltip', 'inline'],
|
|
811
1258
|
};
|
|
@@ -816,6 +1263,8 @@ const CRON_BUILDER_AI_CAPABILITIES = {
|
|
|
816
1263
|
notes: [
|
|
817
1264
|
'presets[] should be merged by cron or label to avoid replacing all entries.',
|
|
818
1265
|
'metadata.previewFrom expects an ISO date string when provided.',
|
|
1266
|
+
'schedule is the canonical scheduler authoring state; value remains the current CRON string compatibility surface.',
|
|
1267
|
+
'Prefer schedule.kind and schedule.recurrence for business intent; use schedule.expression.cron only for custom CRON edits.',
|
|
819
1268
|
],
|
|
820
1269
|
capabilities: [
|
|
821
1270
|
{ path: 'label', category: 'meta', valueKind: 'string', description: 'Field label (if exposed by host).' },
|
|
@@ -848,6 +1297,330 @@ const CRON_BUILDER_AI_CAPABILITIES = {
|
|
|
848
1297
|
{ path: 'metadata.errorPosition', category: 'ui', valueKind: 'enum', allowedValues: ENUMS.errorPosition, description: 'Error display position.' },
|
|
849
1298
|
{ path: 'metadata.showInlineErrors', category: 'ui', valueKind: 'boolean', description: 'Show inline errors.' },
|
|
850
1299
|
{ path: 'metadata.hint', category: 'ui', valueKind: 'string', description: 'Hint text.' },
|
|
1300
|
+
{ path: 'schedule', category: 'schedule', valueKind: 'object', description: 'Canonical scheduler authoring config.' },
|
|
1301
|
+
{ path: 'schedule.kind', category: 'schedule', valueKind: 'enum', allowedValues: ENUMS.scheduleKind, description: 'Business schedule intent.' },
|
|
1302
|
+
{ path: 'schedule.timezone', category: 'i18n', valueKind: 'string', description: 'IANA timezone used by the schedule.' },
|
|
1303
|
+
{ path: 'schedule.locale', category: 'i18n', valueKind: 'string', description: 'Locale used by humanized text and preview.' },
|
|
1304
|
+
{ path: 'schedule.expression', category: 'dialects', valueKind: 'object', description: 'Technical CRON representation.' },
|
|
1305
|
+
{ path: 'schedule.expression.cron', category: 'dialects', valueKind: 'string', description: 'CRON expression for custom or compiled schedules.' },
|
|
1306
|
+
{ path: 'schedule.expression.dialect', category: 'dialects', valueKind: 'enum', allowedValues: ENUMS.cronDialect, description: 'CRON dialect family.' },
|
|
1307
|
+
{ path: 'schedule.expression.seconds', category: 'dialects', valueKind: 'boolean', description: 'Whether the CRON representation includes seconds.' },
|
|
1308
|
+
{ path: 'schedule.recurrence', category: 'recurrence', valueKind: 'object', description: 'Structured recurrence intent.' },
|
|
1309
|
+
{ path: 'schedule.recurrence.once.runAt', category: 'recurrence', valueKind: 'string', description: 'ISO date-time for one-time schedules.' },
|
|
1310
|
+
{ path: 'schedule.recurrence.interval.every', category: 'recurrence', valueKind: 'number', description: 'Interval amount.' },
|
|
1311
|
+
{ path: 'schedule.recurrence.interval.unit', category: 'recurrence', valueKind: 'enum', allowedValues: ENUMS.intervalUnit, description: 'Interval unit.' },
|
|
1312
|
+
{ path: 'schedule.recurrence.daily.times', category: 'recurrence', valueKind: 'array', description: 'Daily run times in HH:mm format.' },
|
|
1313
|
+
{ path: 'schedule.recurrence.daily.onlyWeekdays', category: 'recurrence', valueKind: 'boolean', description: 'Limit daily schedule to Monday through Friday.' },
|
|
1314
|
+
{ path: 'schedule.recurrence.weekly.daysOfWeek', category: 'recurrence', valueKind: 'array', description: 'Weekdays as numbers where 0 is Sunday.' },
|
|
1315
|
+
{ path: 'schedule.recurrence.weekly.times', category: 'recurrence', valueKind: 'array', description: 'Weekly run times in HH:mm format.' },
|
|
1316
|
+
{ path: 'schedule.recurrence.monthly.mode', category: 'recurrence', valueKind: 'enum', allowedValues: ENUMS.monthlyMode, description: 'Monthly recurrence strategy.' },
|
|
1317
|
+
{ path: 'schedule.recurrence.monthly.dayOfMonth', category: 'recurrence', valueKind: 'number', description: 'Day of month for monthly schedules.' },
|
|
1318
|
+
{ path: 'schedule.recurrence.monthly.nth', category: 'recurrence', valueKind: 'number', description: 'Nth weekday occurrence in the month.' },
|
|
1319
|
+
{ path: 'schedule.recurrence.monthly.weekday', category: 'recurrence', valueKind: 'number', description: 'Weekday for nth weekday monthly schedules.' },
|
|
1320
|
+
{ path: 'schedule.recurrence.monthly.times', category: 'recurrence', valueKind: 'array', description: 'Monthly run times in HH:mm format.' },
|
|
1321
|
+
{ path: 'schedule.window.startAt', category: 'policy', valueKind: 'string', description: 'ISO start date-time for schedule validity.' },
|
|
1322
|
+
{ path: 'schedule.window.endAt', category: 'policy', valueKind: 'string', description: 'ISO end date-time for schedule validity.' },
|
|
1323
|
+
{ path: 'schedule.executionPolicy.enabled', category: 'policy', valueKind: 'boolean', description: 'Whether the schedule is active.' },
|
|
1324
|
+
{ path: 'schedule.executionPolicy.misfirePolicy', category: 'policy', valueKind: 'enum', allowedValues: ENUMS.misfirePolicy, description: 'Policy for missed scheduled runs.' },
|
|
1325
|
+
{ path: 'schedule.executionPolicy.concurrencyPolicy', category: 'policy', valueKind: 'enum', allowedValues: ENUMS.concurrencyPolicy, description: 'Policy for overlapping runs.' },
|
|
1326
|
+
{ path: 'schedule.executionPolicy.flexibleWindowMinutes', category: 'policy', valueKind: 'number', description: 'Flexible delivery window in minutes.' },
|
|
1327
|
+
{ path: 'schedule.executionPolicy.jitterMinutes', category: 'policy', valueKind: 'number', description: 'Randomized delay window in minutes.' },
|
|
1328
|
+
{ path: 'schedule.preview.occurrences', category: 'preview', valueKind: 'number', description: 'Number of occurrences to preview.' },
|
|
1329
|
+
{ path: 'schedule.preview.from', category: 'preview', valueKind: 'string', description: 'ISO date-time used as preview start.' },
|
|
1330
|
+
{ path: 'schedule.governance.name', category: 'governance', valueKind: 'string', description: 'Schedule name.' },
|
|
1331
|
+
{ path: 'schedule.governance.description', category: 'governance', valueKind: 'string', description: 'Schedule description.' },
|
|
1332
|
+
{ path: 'schedule.governance.owner', category: 'governance', valueKind: 'string', description: 'Schedule owner.' },
|
|
1333
|
+
{ path: 'schedule.governance.tags', category: 'governance', valueKind: 'array', description: 'Governance tags.' },
|
|
1334
|
+
{ path: 'diagnostics', category: 'validation', valueKind: 'array', description: 'Read-only structured schedule diagnostics.' },
|
|
1335
|
+
{ path: 'preview', category: 'preview', valueKind: 'array', description: 'Read-only structured preview occurrences.' },
|
|
1336
|
+
],
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
const PRAXIS_CRON_BUILDER_AUTHORING_MANIFEST = {
|
|
1340
|
+
schemaVersion: '1.0.0',
|
|
1341
|
+
componentId: 'pdx-cron-builder',
|
|
1342
|
+
ownerPackage: '@praxisui/cron-builder',
|
|
1343
|
+
configSchemaId: 'CronBuilderMetadata',
|
|
1344
|
+
manifestVersion: '1.0.0',
|
|
1345
|
+
runtimeInputs: [
|
|
1346
|
+
{ name: 'metadata', type: 'CronBuilderMetadata', description: 'Builder UI, timezone, locale, presets, validation and preview configuration.' },
|
|
1347
|
+
{ name: 'value', type: 'string | ScheduleAuthoringConfig', description: 'Current CRON string compatibility value or canonical schedule authoring config.' },
|
|
1348
|
+
{ name: 'disabled', type: 'boolean', description: 'ControlValueAccessor disabled state.' },
|
|
1349
|
+
],
|
|
1350
|
+
editableTargets: [
|
|
1351
|
+
{ kind: 'expression', resolver: 'schedule-expression', description: 'Canonical CRON expression under schedule.expression or legacy value.' },
|
|
1352
|
+
{ kind: 'frequency', resolver: 'schedule-kind-and-recurrence', description: 'Business recurrence intent compiled into a canonical expression.' },
|
|
1353
|
+
{ kind: 'timezone', resolver: 'schedule-timezone', description: 'IANA timezone used for preview and schedule metadata.' },
|
|
1354
|
+
{ kind: 'preview', resolver: 'schedule-preview-config', description: 'Read-only preview generation request for next occurrences.' },
|
|
1355
|
+
{ kind: 'validation', resolver: 'schedule-validation-request', description: 'Read-only validation diagnostics request for expression or schedule config.' },
|
|
1356
|
+
{ kind: 'preset', resolver: 'cron-preset-by-label-or-expression', description: 'Preset entry in metadata.presets[] or top-level presets[].' },
|
|
1357
|
+
],
|
|
1358
|
+
operations: [
|
|
1359
|
+
{
|
|
1360
|
+
operationId: 'cron.expression.set',
|
|
1361
|
+
title: 'Set CRON expression',
|
|
1362
|
+
scope: 'global',
|
|
1363
|
+
targetKind: 'expression',
|
|
1364
|
+
target: { kind: 'expression', resolver: 'schedule-expression', ambiguityPolicy: 'fail', required: false },
|
|
1365
|
+
inputSchema: {
|
|
1366
|
+
type: 'object',
|
|
1367
|
+
required: ['cron'],
|
|
1368
|
+
properties: {
|
|
1369
|
+
cron: { type: 'string' },
|
|
1370
|
+
dialect: { enum: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'] },
|
|
1371
|
+
seconds: { type: 'boolean' },
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
effects: [{
|
|
1375
|
+
kind: 'compile-domain-patch',
|
|
1376
|
+
handler: 'cron-expression-set',
|
|
1377
|
+
handlerContract: {
|
|
1378
|
+
reads: ['schedule.expression', 'schedule.timezone', 'metadata.timezone', 'value'],
|
|
1379
|
+
writes: ['schedule.kind', 'schedule.expression', 'value', 'diagnostics'],
|
|
1380
|
+
identityKeys: ['schedule.expression.cron', 'schedule.expression.dialect'],
|
|
1381
|
+
inputSchema: {
|
|
1382
|
+
type: 'object',
|
|
1383
|
+
required: ['cron'],
|
|
1384
|
+
properties: {
|
|
1385
|
+
cron: { type: 'string' },
|
|
1386
|
+
dialect: { enum: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'] },
|
|
1387
|
+
seconds: { type: 'boolean' },
|
|
1388
|
+
},
|
|
1389
|
+
},
|
|
1390
|
+
failureModes: ['invalid-expression', 'invalid-dialect', 'seconds-field-mismatch'],
|
|
1391
|
+
description: 'Validates and normalizes an explicit CRON expression before writing schedule.expression and the compatibility value.',
|
|
1392
|
+
},
|
|
1393
|
+
}],
|
|
1394
|
+
destructive: false,
|
|
1395
|
+
requiresConfirmation: false,
|
|
1396
|
+
validators: ['cron-expression-valid', 'cron-dialect-compatible', 'editor-runtime-round-trip'],
|
|
1397
|
+
affectedPaths: ['schedule.kind', 'schedule.expression', 'schedule.expression.cron', 'schedule.expression.dialect', 'schedule.expression.seconds', 'value', 'diagnostics'],
|
|
1398
|
+
submissionImpact: 'affects-submission',
|
|
1399
|
+
preconditions: ['config-initialized'],
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
operationId: 'cron.frequency.set',
|
|
1403
|
+
title: 'Set schedule frequency',
|
|
1404
|
+
scope: 'global',
|
|
1405
|
+
targetKind: 'frequency',
|
|
1406
|
+
target: { kind: 'frequency', resolver: 'schedule-kind-and-recurrence', ambiguityPolicy: 'fail', required: false },
|
|
1407
|
+
inputSchema: {
|
|
1408
|
+
type: 'object',
|
|
1409
|
+
required: ['kind', 'recurrence'],
|
|
1410
|
+
properties: {
|
|
1411
|
+
kind: { enum: ['once', 'interval', 'daily', 'weekly', 'monthly', 'customCron'] },
|
|
1412
|
+
recurrence: { type: 'object' },
|
|
1413
|
+
dialect: { enum: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'] },
|
|
1414
|
+
},
|
|
1415
|
+
},
|
|
1416
|
+
effects: [{
|
|
1417
|
+
kind: 'compile-domain-patch',
|
|
1418
|
+
handler: 'cron-frequency-to-expression',
|
|
1419
|
+
handlerContract: {
|
|
1420
|
+
reads: ['schedule.kind', 'schedule.recurrence', 'schedule.timezone', 'schedule.expression.dialect'],
|
|
1421
|
+
writes: ['schedule.kind', 'schedule.recurrence', 'schedule.expression', 'value'],
|
|
1422
|
+
identityKeys: ['schedule.kind'],
|
|
1423
|
+
inputSchema: {
|
|
1424
|
+
type: 'object',
|
|
1425
|
+
required: ['kind', 'recurrence'],
|
|
1426
|
+
properties: {
|
|
1427
|
+
kind: { enum: ['once', 'interval', 'daily', 'weekly', 'monthly', 'customCron'] },
|
|
1428
|
+
recurrence: { type: 'object' },
|
|
1429
|
+
dialect: { enum: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'] },
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
failureModes: ['unsupported-kind', 'non-portable-frequency', 'invalid-recurrence', 'cron-compile-failed'],
|
|
1433
|
+
description: 'Compiles structured recurrence intent through compileScheduleExpression and updates the canonical schedule expression only when diagnostics have no errors.',
|
|
1434
|
+
},
|
|
1435
|
+
}],
|
|
1436
|
+
destructive: false,
|
|
1437
|
+
requiresConfirmation: false,
|
|
1438
|
+
validators: ['frequency-maps-to-canonical-expression', 'cron-expression-valid', 'diagnostics-before-patch'],
|
|
1439
|
+
affectedPaths: ['schedule.kind', 'schedule.recurrence', 'schedule.expression', 'value'],
|
|
1440
|
+
submissionImpact: 'affects-submission',
|
|
1441
|
+
preconditions: ['config-initialized'],
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
operationId: 'cron.timezone.set',
|
|
1445
|
+
title: 'Set schedule timezone',
|
|
1446
|
+
scope: 'global',
|
|
1447
|
+
targetKind: 'timezone',
|
|
1448
|
+
target: { kind: 'timezone', resolver: 'schedule-timezone', ambiguityPolicy: 'fail', required: false },
|
|
1449
|
+
inputSchema: { type: 'object', required: ['timezone'], properties: { timezone: { type: 'string' } } },
|
|
1450
|
+
effects: [{
|
|
1451
|
+
kind: 'compile-domain-patch',
|
|
1452
|
+
handler: 'cron-timezone-set',
|
|
1453
|
+
handlerContract: {
|
|
1454
|
+
reads: ['value', 'schedule', 'metadata.timezone', 'metadata.previewOccurrences', 'metadata.previewFrom', 'metadata.locale'],
|
|
1455
|
+
writes: ['schedule.timezone', 'metadata.timezone', 'preview', 'diagnostics'],
|
|
1456
|
+
identityKeys: ['schedule.expression.cron', 'schedule.timezone'],
|
|
1457
|
+
inputSchema: { type: 'object', required: ['timezone'], properties: { timezone: { type: 'string' } } },
|
|
1458
|
+
failureModes: ['invalid-timezone', 'invalid-expression', 'preview-generation-failed'],
|
|
1459
|
+
description: 'Sets the IANA timezone used by schedule and metadata, then regenerates preview/diagnostics from the same expression.',
|
|
1460
|
+
},
|
|
1461
|
+
}],
|
|
1462
|
+
destructive: false,
|
|
1463
|
+
requiresConfirmation: false,
|
|
1464
|
+
validators: ['timezone-valid', 'preview-matches-expression', 'editor-runtime-round-trip'],
|
|
1465
|
+
affectedPaths: ['schedule.timezone', 'metadata.timezone', 'preview', 'diagnostics'],
|
|
1466
|
+
submissionImpact: 'affects-submission',
|
|
1467
|
+
preconditions: ['config-initialized'],
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
operationId: 'cron.preset.apply',
|
|
1471
|
+
title: 'Apply CRON preset',
|
|
1472
|
+
scope: 'meta',
|
|
1473
|
+
targetKind: 'preset',
|
|
1474
|
+
target: { kind: 'preset', resolver: 'cron-preset-by-label-or-expression', ambiguityPolicy: 'fail', required: true },
|
|
1475
|
+
inputSchema: {
|
|
1476
|
+
type: 'object',
|
|
1477
|
+
required: ['labelOrCron'],
|
|
1478
|
+
properties: {
|
|
1479
|
+
labelOrCron: { type: 'string' },
|
|
1480
|
+
timezone: { type: 'string' },
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
effects: [{
|
|
1484
|
+
kind: 'compile-domain-patch',
|
|
1485
|
+
handler: 'cron-preset-apply',
|
|
1486
|
+
handlerContract: {
|
|
1487
|
+
reads: ['metadata.presets[]', 'presets[]', 'schedule.timezone', 'metadata.timezone'],
|
|
1488
|
+
writes: ['schedule.kind', 'schedule.expression', 'schedule.timezone', 'value'],
|
|
1489
|
+
identityKeys: ['metadata.presets[].label', 'metadata.presets[].cron', 'presets[].label', 'presets[].cron'],
|
|
1490
|
+
inputSchema: {
|
|
1491
|
+
type: 'object',
|
|
1492
|
+
required: ['labelOrCron'],
|
|
1493
|
+
properties: {
|
|
1494
|
+
labelOrCron: { type: 'string' },
|
|
1495
|
+
timezone: { type: 'string' },
|
|
1496
|
+
},
|
|
1497
|
+
},
|
|
1498
|
+
failureModes: ['preset-not-found', 'preset-cron-invalid', 'timezone-invalid'],
|
|
1499
|
+
description: 'Resolves a preset by label or cron, validates it through normalizeScheduleValue/createSchedulePreview, then applies the canonical expression.',
|
|
1500
|
+
},
|
|
1501
|
+
}],
|
|
1502
|
+
destructive: false,
|
|
1503
|
+
requiresConfirmation: false,
|
|
1504
|
+
validators: ['preset-exists', 'preset-maps-to-canonical-expression', 'cron-expression-valid'],
|
|
1505
|
+
affectedPaths: ['schedule.kind', 'schedule.expression', 'schedule.timezone', 'value'],
|
|
1506
|
+
submissionImpact: 'affects-submission',
|
|
1507
|
+
preconditions: ['config-initialized', 'target-preset-exists'],
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
operationId: 'cron.validate',
|
|
1511
|
+
title: 'Validate schedule',
|
|
1512
|
+
scope: 'global',
|
|
1513
|
+
targetKind: 'validation',
|
|
1514
|
+
target: { kind: 'validation', resolver: 'schedule-validation-request', ambiguityPolicy: 'fail', required: false },
|
|
1515
|
+
inputSchema: {
|
|
1516
|
+
type: 'object',
|
|
1517
|
+
properties: {
|
|
1518
|
+
cron: { type: 'string' },
|
|
1519
|
+
schedule: { type: 'object' },
|
|
1520
|
+
timezone: { type: 'string' },
|
|
1521
|
+
dialect: { enum: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'] },
|
|
1522
|
+
},
|
|
1523
|
+
},
|
|
1524
|
+
effects: [{
|
|
1525
|
+
kind: 'compile-domain-patch',
|
|
1526
|
+
handler: 'cron-validation-diagnostics',
|
|
1527
|
+
handlerContract: {
|
|
1528
|
+
reads: ['value', 'schedule', 'metadata.timezone'],
|
|
1529
|
+
writes: ['diagnostics'],
|
|
1530
|
+
identityKeys: ['schedule.expression.cron', 'schedule.timezone'],
|
|
1531
|
+
inputSchema: {
|
|
1532
|
+
type: 'object',
|
|
1533
|
+
properties: {
|
|
1534
|
+
cron: { type: 'string' },
|
|
1535
|
+
schedule: { type: 'object' },
|
|
1536
|
+
timezone: { type: 'string' },
|
|
1537
|
+
dialect: { enum: ['unix', 'quartz', 'aws-eventbridge', 'kubernetes', 'github-actions', 'gcp-scheduler'] },
|
|
1538
|
+
},
|
|
1539
|
+
},
|
|
1540
|
+
failureModes: ['invalid-expression', 'invalid-timezone', 'invalid-recurrence'],
|
|
1541
|
+
description: 'Runs normalizeScheduleValue and validateScheduleAuthoringConfig and returns diagnostics without mutating schedule/value paths.',
|
|
1542
|
+
},
|
|
1543
|
+
}],
|
|
1544
|
+
destructive: false,
|
|
1545
|
+
requiresConfirmation: false,
|
|
1546
|
+
validators: ['invalid-schedules-return-diagnostics', 'cron-expression-valid', 'timezone-valid'],
|
|
1547
|
+
affectedPaths: ['diagnostics'],
|
|
1548
|
+
submissionImpact: 'none',
|
|
1549
|
+
preconditions: ['config-initialized'],
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
operationId: 'cron.preview.generate',
|
|
1553
|
+
title: 'Generate schedule preview',
|
|
1554
|
+
scope: 'global',
|
|
1555
|
+
targetKind: 'preview',
|
|
1556
|
+
target: { kind: 'preview', resolver: 'schedule-preview-config', ambiguityPolicy: 'fail', required: false },
|
|
1557
|
+
inputSchema: {
|
|
1558
|
+
type: 'object',
|
|
1559
|
+
properties: {
|
|
1560
|
+
occurrences: { type: 'number' },
|
|
1561
|
+
from: { type: 'string' },
|
|
1562
|
+
timezone: { type: 'string' },
|
|
1563
|
+
locale: { type: 'string' },
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
effects: [{
|
|
1567
|
+
kind: 'compile-domain-patch',
|
|
1568
|
+
handler: 'cron-preview-generate',
|
|
1569
|
+
handlerContract: {
|
|
1570
|
+
reads: ['value', 'schedule', 'metadata.previewOccurrences', 'metadata.previewFrom', 'metadata.timezone', 'metadata.locale'],
|
|
1571
|
+
writes: ['preview', 'diagnostics'],
|
|
1572
|
+
identityKeys: ['schedule.expression.cron', 'schedule.timezone', 'metadata.previewFrom'],
|
|
1573
|
+
inputSchema: {
|
|
1574
|
+
type: 'object',
|
|
1575
|
+
properties: {
|
|
1576
|
+
occurrences: { type: 'number' },
|
|
1577
|
+
from: { type: 'string' },
|
|
1578
|
+
timezone: { type: 'string' },
|
|
1579
|
+
locale: { type: 'string' },
|
|
1580
|
+
},
|
|
1581
|
+
},
|
|
1582
|
+
failureModes: ['invalid-expression', 'invalid-timezone', 'preview-empty', 'diagnostics-contain-errors'],
|
|
1583
|
+
description: 'Runs createSchedulePreview and returns read-only preview occurrences plus diagnostics; it does not patch the schedule when validation fails.',
|
|
1584
|
+
},
|
|
1585
|
+
}],
|
|
1586
|
+
destructive: false,
|
|
1587
|
+
requiresConfirmation: false,
|
|
1588
|
+
validators: ['preview-matches-expression', 'timezone-valid', 'invalid-schedules-return-diagnostics'],
|
|
1589
|
+
affectedPaths: ['preview', 'diagnostics'],
|
|
1590
|
+
submissionImpact: 'none',
|
|
1591
|
+
preconditions: ['config-initialized'],
|
|
1592
|
+
},
|
|
1593
|
+
],
|
|
1594
|
+
validators: [
|
|
1595
|
+
{ validatorId: 'cron-expression-valid', level: 'error', code: 'PCRON001', description: 'CRON expressions must pass the canonical cron-runtime parser for the selected dialect before schedule/value paths are patched.' },
|
|
1596
|
+
{ validatorId: 'cron-dialect-compatible', level: 'error', code: 'PCRON002', description: 'Field count, seconds support and dialect-specific rules must match CRON_DIALECTS.' },
|
|
1597
|
+
{ validatorId: 'timezone-valid', level: 'error', code: 'PCRON003', description: 'Timezone must be a valid IANA timezone accepted by Intl.DateTimeFormat and schedule preview.' },
|
|
1598
|
+
{ validatorId: 'frequency-maps-to-canonical-expression', level: 'error', code: 'PCRON004', description: 'Structured frequency intent must compile through compileScheduleExpression into the canonical expression when portable.' },
|
|
1599
|
+
{ validatorId: 'preset-exists', level: 'error', code: 'PCRON005', description: 'Preset operations must resolve an existing preset by label or cron expression.' },
|
|
1600
|
+
{ validatorId: 'preset-maps-to-canonical-expression', level: 'error', code: 'PCRON006', description: 'Preset cron values must normalize into ScheduleAuthoringConfig and a valid canonical expression.' },
|
|
1601
|
+
{ validatorId: 'preview-matches-expression', level: 'error', code: 'PCRON007', description: 'Preview occurrences must be generated from the same expression, timezone and from-date requested by authoring.' },
|
|
1602
|
+
{ validatorId: 'invalid-schedules-return-diagnostics', level: 'error', code: 'PCRON008', description: 'Validation and preview requests for invalid schedules must return diagnostics and must not patch schedule/value fields.' },
|
|
1603
|
+
{ validatorId: 'diagnostics-before-patch', level: 'error', code: 'PCRON009', description: 'Operations that compile recurrence or presets must evaluate diagnostics before mutating canonical schedule/value paths.' },
|
|
1604
|
+
{ validatorId: 'editor-runtime-round-trip', level: 'error', code: 'PCRON010', description: 'ControlValueAccessor, runtime preview and AI adapter snapshots must preserve expression and timezone.' },
|
|
1605
|
+
],
|
|
1606
|
+
roundTripRequirements: [
|
|
1607
|
+
'Expression and timezone edits must round-trip through ControlValueAccessor, normalizeScheduleValue and createSchedulePreview without losing the selected timezone.',
|
|
1608
|
+
'Structured frequency authoring must use ScheduleAuthoringConfig and compileScheduleExpression as the canonical source of recurrence semantics.',
|
|
1609
|
+
'Validation and preview operations are read-only: invalid schedules return diagnostics and must not mutate schedule/value paths.',
|
|
1610
|
+
'Preset operations must resolve stable label/cron identities and validate the resulting expression before applying it.',
|
|
1611
|
+
'Preview generation must use the same expression, timezone, locale, occurrences and from-date that are visible in the schedule config or metadata input.',
|
|
1612
|
+
],
|
|
1613
|
+
examples: [
|
|
1614
|
+
{ id: 'weekday-0800', request: 'Run every weekday at 08:00.', operationId: 'cron.frequency.set', params: { kind: 'daily', recurrence: { daily: { times: ['08:00'], onlyWeekdays: true } }, dialect: 'unix' }, isPositive: true },
|
|
1615
|
+
{ id: 'every-15-minutes', request: 'Run every 15 minutes.', operationId: 'cron.frequency.set', params: { kind: 'interval', recurrence: { interval: { every: 15, unit: 'minutes' } }, dialect: 'unix' }, isPositive: true },
|
|
1616
|
+
{ id: 'monthly-first-day', request: 'Run monthly on the first day at 09:00.', operationId: 'cron.frequency.set', params: { kind: 'monthly', recurrence: { monthly: { mode: 'dayOfMonth', dayOfMonth: 1, times: ['09:00'] } }, dialect: 'unix' }, isPositive: true },
|
|
1617
|
+
{ id: 'set-explicit-expression', request: 'Use cron expression 0 8 * * 1-5.', operationId: 'cron.expression.set', params: { cron: '0 8 * * 1-5', dialect: 'unix', seconds: false }, isPositive: true },
|
|
1618
|
+
{ id: 'change-timezone-preview', request: 'Change timezone to America/Sao_Paulo and preview the next runs.', operationId: 'cron.timezone.set', params: { timezone: 'America/Sao_Paulo' }, isPositive: true },
|
|
1619
|
+
{ id: 'preview-next-five', request: 'Preview the next five runs from 2026-04-20T00:00:00Z.', operationId: 'cron.preview.generate', params: { occurrences: 5, from: '2026-04-20T00:00:00.000Z', timezone: 'UTC' }, isPositive: true },
|
|
1620
|
+
{ id: 'apply-business-hours-preset', request: 'Apply the business hours preset.', operationId: 'cron.preset.apply', target: 'business hours', params: { labelOrCron: 'business hours' }, isPositive: true },
|
|
1621
|
+
{ id: 'reject-impossible-expression', request: 'Use cron expression 99 99 * * *.', operationId: 'cron.expression.set', params: { cron: '99 99 * * *', dialect: 'unix' }, isPositive: false },
|
|
1622
|
+
{ id: 'diagnose-invalid-expression', request: 'Validate 99 99 * * * and explain why it is invalid.', operationId: 'cron.validate', params: { cron: '99 99 * * *', dialect: 'unix' }, isPositive: true },
|
|
1623
|
+
{ id: 'reject-invalid-timezone', request: 'Change timezone to Mars/Olympus.', operationId: 'cron.timezone.set', params: { timezone: 'Mars/Olympus' }, isPositive: false },
|
|
851
1624
|
],
|
|
852
1625
|
};
|
|
853
1626
|
|
|
@@ -859,4 +1632,4 @@ const CRON_BUILDER_AI_CAPABILITIES = {
|
|
|
859
1632
|
* Generated bundle index. Do not edit.
|
|
860
1633
|
*/
|
|
861
1634
|
|
|
862
|
-
export { CRON_BUILDER_AI_CAPABILITIES, PDX_CRON_BUILDER_DOC_META_LEGACY, PdxCronBuilderComponent };
|
|
1635
|
+
export { CRON_BUILDER_AI_CAPABILITIES, CRON_DIALECTS, PDX_CRON_BUILDER_DOC_META_LEGACY, PRAXIS_CRON_BUILDER_AUTHORING_MANIFEST, PdxCronBuilderComponent, SCHEDULE_AUTHORING_CONFIG_VERSION, compileScheduleExpression, createSchedulePreview, getCronDialectDefinition, inferCronDialect, normalizeScheduleValue, validateScheduleAuthoringConfig };
|