@djangocfg/ui-tools 2.1.130 → 2.1.131
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 +57 -1
- package/dist/CronScheduler.client-5UEBG4EY.mjs +67 -0
- package/dist/CronScheduler.client-5UEBG4EY.mjs.map +1 -0
- package/dist/CronScheduler.client-ZDNFXYWJ.cjs +72 -0
- package/dist/CronScheduler.client-ZDNFXYWJ.cjs.map +1 -0
- package/dist/chunk-JFGLA6DT.cjs +1013 -0
- package/dist/chunk-JFGLA6DT.cjs.map +1 -0
- package/dist/chunk-MQDWUBVX.mjs +993 -0
- package/dist/chunk-MQDWUBVX.mjs.map +1 -0
- package/dist/index.cjs +109 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -1
- package/dist/index.d.ts +280 -1
- package/dist/index.mjs +32 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +5 -0
- package/src/tools/CronScheduler/CronScheduler.client.tsx +140 -0
- package/src/tools/CronScheduler/CronScheduler.story.tsx +220 -0
- package/src/tools/CronScheduler/components/CronCheatsheet.tsx +101 -0
- package/src/tools/CronScheduler/components/CustomInput.tsx +67 -0
- package/src/tools/CronScheduler/components/DayChips.tsx +130 -0
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +143 -0
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +103 -0
- package/src/tools/CronScheduler/components/ScheduleTypeSelector.tsx +57 -0
- package/src/tools/CronScheduler/components/TimeSelector.tsx +132 -0
- package/src/tools/CronScheduler/components/index.ts +24 -0
- package/src/tools/CronScheduler/context/CronSchedulerContext.tsx +237 -0
- package/src/tools/CronScheduler/context/hooks.ts +86 -0
- package/src/tools/CronScheduler/context/index.ts +18 -0
- package/src/tools/CronScheduler/index.tsx +91 -0
- package/src/tools/CronScheduler/lazy.tsx +67 -0
- package/src/tools/CronScheduler/types/index.ts +112 -0
- package/src/tools/CronScheduler/utils/cron-builder.ts +100 -0
- package/src/tools/CronScheduler/utils/cron-humanize.ts +218 -0
- package/src/tools/CronScheduler/utils/cron-parser.ts +188 -0
- package/src/tools/CronScheduler/utils/index.ts +12 -0
- package/src/tools/index.ts +36 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CronScheduler Hooks
|
|
5
|
+
*
|
|
6
|
+
* Selective subscription hooks for optimal re-renders.
|
|
7
|
+
* Each hook returns only the data it needs, preventing
|
|
8
|
+
* unnecessary re-renders when unrelated state changes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useMemo } from 'react';
|
|
12
|
+
import { useCronSchedulerContext } from './CronSchedulerContext';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook for schedule type selection
|
|
16
|
+
* Re-renders only when type changes
|
|
17
|
+
*/
|
|
18
|
+
export function useCronType() {
|
|
19
|
+
const { type, setType } = useCronSchedulerContext();
|
|
20
|
+
return useMemo(() => ({ type, setType }), [type, setType]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Hook for time selection
|
|
25
|
+
* Re-renders only when hour/minute changes
|
|
26
|
+
*/
|
|
27
|
+
export function useCronTime() {
|
|
28
|
+
const { hour, minute, setTime } = useCronSchedulerContext();
|
|
29
|
+
return useMemo(() => ({ hour, minute, setTime }), [hour, minute, setTime]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hook for week days selection
|
|
34
|
+
* Re-renders only when weekDays changes
|
|
35
|
+
*/
|
|
36
|
+
export function useCronWeekDays() {
|
|
37
|
+
const { weekDays, toggleWeekDay, setWeekDays } = useCronSchedulerContext();
|
|
38
|
+
return useMemo(
|
|
39
|
+
() => ({ weekDays, toggleWeekDay, setWeekDays }),
|
|
40
|
+
[weekDays, toggleWeekDay, setWeekDays]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hook for month days selection
|
|
46
|
+
* Re-renders only when monthDays changes
|
|
47
|
+
*/
|
|
48
|
+
export function useCronMonthDays() {
|
|
49
|
+
const { monthDays, toggleMonthDay, setMonthDays } = useCronSchedulerContext();
|
|
50
|
+
return useMemo(
|
|
51
|
+
() => ({ monthDays, toggleMonthDay, setMonthDays }),
|
|
52
|
+
[monthDays, toggleMonthDay, setMonthDays]
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hook for custom cron input
|
|
58
|
+
* Re-renders only when customCron/isValid changes
|
|
59
|
+
*/
|
|
60
|
+
export function useCronCustom() {
|
|
61
|
+
const { customCron, isValid, setCustomCron } = useCronSchedulerContext();
|
|
62
|
+
return useMemo(
|
|
63
|
+
() => ({ customCron, isValid, setCustomCron }),
|
|
64
|
+
[customCron, isValid, setCustomCron]
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook for preview display
|
|
70
|
+
* Re-renders only when computed values change
|
|
71
|
+
*/
|
|
72
|
+
export function useCronPreview() {
|
|
73
|
+
const { cronExpression, humanDescription, isValid } = useCronSchedulerContext();
|
|
74
|
+
return useMemo(
|
|
75
|
+
() => ({ cronExpression, humanDescription, isValid }),
|
|
76
|
+
[cronExpression, humanDescription, isValid]
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Full context access
|
|
82
|
+
* Use sparingly - causes re-render on any state change
|
|
83
|
+
*/
|
|
84
|
+
export function useCronScheduler() {
|
|
85
|
+
return useCronSchedulerContext();
|
|
86
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronScheduler Context
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
CronSchedulerProvider,
|
|
7
|
+
useCronSchedulerContext,
|
|
8
|
+
} from './CronSchedulerContext';
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
useCronType,
|
|
12
|
+
useCronTime,
|
|
13
|
+
useCronWeekDays,
|
|
14
|
+
useCronMonthDays,
|
|
15
|
+
useCronCustom,
|
|
16
|
+
useCronPreview,
|
|
17
|
+
useCronScheduler,
|
|
18
|
+
} from './hooks';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CronScheduler
|
|
5
|
+
*
|
|
6
|
+
* Compact cron expression builder following Apple HIG principles.
|
|
7
|
+
* Lazy-loaded for optimal bundle size (~15KB).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { CronScheduler } from '@djangocfg/ui-tools';
|
|
11
|
+
*
|
|
12
|
+
* <CronScheduler
|
|
13
|
+
* value={cron}
|
|
14
|
+
* onChange={setCron}
|
|
15
|
+
* showPreview
|
|
16
|
+
* />
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React, { lazy, Suspense } from 'react';
|
|
20
|
+
import { LoadingFallback } from '../../components';
|
|
21
|
+
import type { CronSchedulerProps } from './types';
|
|
22
|
+
|
|
23
|
+
// Lazy load the client component
|
|
24
|
+
const CronSchedulerClient = lazy(() => import('./CronScheduler.client'));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* CronScheduler with Suspense wrapper
|
|
28
|
+
*/
|
|
29
|
+
export function CronScheduler(props: CronSchedulerProps) {
|
|
30
|
+
return (
|
|
31
|
+
<Suspense fallback={<CronSchedulerFallback />}>
|
|
32
|
+
<CronSchedulerClient {...props} />
|
|
33
|
+
</Suspense>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Loading fallback for CronScheduler
|
|
39
|
+
*/
|
|
40
|
+
function CronSchedulerFallback() {
|
|
41
|
+
return (
|
|
42
|
+
<LoadingFallback
|
|
43
|
+
minHeight={120}
|
|
44
|
+
showText={false}
|
|
45
|
+
className="rounded-lg"
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Re-export types
|
|
51
|
+
export type {
|
|
52
|
+
CronSchedulerProps,
|
|
53
|
+
ScheduleType,
|
|
54
|
+
WeekDay,
|
|
55
|
+
MonthDay,
|
|
56
|
+
CronSchedulerState,
|
|
57
|
+
CronSchedulerContextValue,
|
|
58
|
+
} from './types';
|
|
59
|
+
|
|
60
|
+
// Re-export context and hooks for advanced usage
|
|
61
|
+
export {
|
|
62
|
+
CronSchedulerProvider,
|
|
63
|
+
useCronSchedulerContext,
|
|
64
|
+
useCronType,
|
|
65
|
+
useCronTime,
|
|
66
|
+
useCronWeekDays,
|
|
67
|
+
useCronMonthDays,
|
|
68
|
+
useCronCustom,
|
|
69
|
+
useCronPreview,
|
|
70
|
+
useCronScheduler,
|
|
71
|
+
} from './context';
|
|
72
|
+
|
|
73
|
+
// Re-export utilities
|
|
74
|
+
export {
|
|
75
|
+
buildCron,
|
|
76
|
+
parseCron,
|
|
77
|
+
isValidCron,
|
|
78
|
+
humanizeCron,
|
|
79
|
+
} from './utils';
|
|
80
|
+
|
|
81
|
+
// Re-export components for custom compositions
|
|
82
|
+
export {
|
|
83
|
+
ScheduleTypeSelector,
|
|
84
|
+
TimeSelector,
|
|
85
|
+
DayChips,
|
|
86
|
+
MonthDayGrid,
|
|
87
|
+
CustomInput,
|
|
88
|
+
SchedulePreview,
|
|
89
|
+
} from './components';
|
|
90
|
+
|
|
91
|
+
export default CronScheduler;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy-loaded CronScheduler Component
|
|
5
|
+
*
|
|
6
|
+
* CronScheduler (~15KB) is loaded only when component is rendered.
|
|
7
|
+
* Use this for automatic code-splitting with Suspense fallback.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { LazyCronScheduler } from '@djangocfg/ui-tools';
|
|
11
|
+
*
|
|
12
|
+
* <LazyCronScheduler
|
|
13
|
+
* value={cron}
|
|
14
|
+
* onChange={setCron}
|
|
15
|
+
* />
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createLazyComponent, LoadingFallback } from '../../components';
|
|
19
|
+
import type { CronSchedulerProps } from './types';
|
|
20
|
+
|
|
21
|
+
// Re-export types
|
|
22
|
+
export type {
|
|
23
|
+
CronSchedulerProps,
|
|
24
|
+
ScheduleType,
|
|
25
|
+
WeekDay,
|
|
26
|
+
MonthDay,
|
|
27
|
+
CronSchedulerState,
|
|
28
|
+
} from './types';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* LazyCronScheduler - Lazy-loaded cron expression builder
|
|
32
|
+
*
|
|
33
|
+
* Automatically shows loading state while CronScheduler loads (~15KB).
|
|
34
|
+
* Uses createLazyComponent factory for optimal code-splitting.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Basic usage
|
|
38
|
+
* <LazyCronScheduler
|
|
39
|
+
* value="0 9 * * *"
|
|
40
|
+
* onChange={handleChange}
|
|
41
|
+
* />
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // With all options
|
|
45
|
+
* <LazyCronScheduler
|
|
46
|
+
* value={cron}
|
|
47
|
+
* onChange={setCron}
|
|
48
|
+
* defaultType="weekly"
|
|
49
|
+
* showPreview
|
|
50
|
+
* showCronExpression
|
|
51
|
+
* allowCopy
|
|
52
|
+
* timeFormat="24h"
|
|
53
|
+
* />
|
|
54
|
+
*/
|
|
55
|
+
export const LazyCronScheduler = createLazyComponent<CronSchedulerProps>(
|
|
56
|
+
() => import('./CronScheduler.client'),
|
|
57
|
+
{
|
|
58
|
+
displayName: 'LazyCronScheduler',
|
|
59
|
+
fallback: (
|
|
60
|
+
<LoadingFallback
|
|
61
|
+
minHeight={120}
|
|
62
|
+
showText={false}
|
|
63
|
+
className="rounded-lg"
|
|
64
|
+
/>
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronScheduler Types
|
|
3
|
+
*
|
|
4
|
+
* Unix 5-field cron format: minute hour day-of-month month day-of-week
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Base Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export type ScheduleType = 'daily' | 'weekly' | 'monthly' | 'custom';
|
|
12
|
+
|
|
13
|
+
/** Day of week: 0 = Sunday, 1 = Monday, ..., 6 = Saturday */
|
|
14
|
+
export type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
15
|
+
|
|
16
|
+
/** Day of month: 1-31 */
|
|
17
|
+
export type MonthDay =
|
|
18
|
+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
|
|
19
|
+
| 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20
|
|
20
|
+
| 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31;
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// State Types
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export interface CronSchedulerState {
|
|
27
|
+
/** Current schedule type */
|
|
28
|
+
type: ScheduleType;
|
|
29
|
+
/** Hour (0-23) */
|
|
30
|
+
hour: number;
|
|
31
|
+
/** Minute (0-59) */
|
|
32
|
+
minute: number;
|
|
33
|
+
/** Selected week days (for weekly type) */
|
|
34
|
+
weekDays: WeekDay[];
|
|
35
|
+
/** Selected month days (for monthly type) */
|
|
36
|
+
monthDays: MonthDay[];
|
|
37
|
+
/** Raw cron expression (for custom type) */
|
|
38
|
+
customCron: string;
|
|
39
|
+
/** Whether current state produces valid cron */
|
|
40
|
+
isValid: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CronSchedulerComputed {
|
|
44
|
+
/** Generated cron expression */
|
|
45
|
+
cronExpression: string;
|
|
46
|
+
/** Human-readable description */
|
|
47
|
+
humanDescription: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Action Types
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export interface CronSchedulerActions {
|
|
55
|
+
/** Set schedule type */
|
|
56
|
+
setType: (type: ScheduleType) => void;
|
|
57
|
+
/** Set time (hour and minute) */
|
|
58
|
+
setTime: (hour: number, minute: number) => void;
|
|
59
|
+
/** Toggle a week day selection */
|
|
60
|
+
toggleWeekDay: (day: WeekDay) => void;
|
|
61
|
+
/** Set all week days at once */
|
|
62
|
+
setWeekDays: (days: WeekDay[]) => void;
|
|
63
|
+
/** Toggle a month day selection */
|
|
64
|
+
toggleMonthDay: (day: MonthDay) => void;
|
|
65
|
+
/** Set all month days at once */
|
|
66
|
+
setMonthDays: (days: MonthDay[]) => void;
|
|
67
|
+
/** Set custom cron expression */
|
|
68
|
+
setCustomCron: (cron: string) => void;
|
|
69
|
+
/** Reset to default state */
|
|
70
|
+
reset: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Context Types
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
export interface CronSchedulerContextValue
|
|
78
|
+
extends CronSchedulerState,
|
|
79
|
+
CronSchedulerComputed,
|
|
80
|
+
CronSchedulerActions {}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Component Props
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
export interface CronSchedulerProps {
|
|
87
|
+
/** Current cron expression (Unix 5-field format) */
|
|
88
|
+
value?: string;
|
|
89
|
+
/** Callback when schedule changes */
|
|
90
|
+
onChange?: (cron: string) => void;
|
|
91
|
+
/** Initial schedule type (default: 'daily') */
|
|
92
|
+
defaultType?: ScheduleType;
|
|
93
|
+
/** Show human-readable preview (default: true) */
|
|
94
|
+
showPreview?: boolean;
|
|
95
|
+
/** Show raw cron expression in preview (default: false) */
|
|
96
|
+
showCronExpression?: boolean;
|
|
97
|
+
/** Allow copying cron expression (default: false) */
|
|
98
|
+
allowCopy?: boolean;
|
|
99
|
+
/** 12h or 24h time format (default: '24h') */
|
|
100
|
+
timeFormat?: '12h' | '24h';
|
|
101
|
+
/** Disable all interactions */
|
|
102
|
+
disabled?: boolean;
|
|
103
|
+
/** Additional CSS classes */
|
|
104
|
+
className?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface CronSchedulerProviderProps {
|
|
108
|
+
children: React.ReactNode;
|
|
109
|
+
value?: string;
|
|
110
|
+
onChange?: (cron: string) => void;
|
|
111
|
+
defaultType?: ScheduleType;
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron Builder
|
|
3
|
+
*
|
|
4
|
+
* Converts CronSchedulerState to Unix 5-field cron expression
|
|
5
|
+
* Format: minute hour day-of-month month day-of-week
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CronSchedulerState } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds Unix 5-field cron expression from state
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Daily at 9:00
|
|
15
|
+
* buildCron({ type: 'daily', hour: 9, minute: 0, ... }) // '0 9 * * *'
|
|
16
|
+
*
|
|
17
|
+
* // Weekly on Mon, Wed, Fri at 14:30
|
|
18
|
+
* buildCron({ type: 'weekly', hour: 14, minute: 30, weekDays: [1, 3, 5], ... }) // '30 14 * * 1,3,5'
|
|
19
|
+
*
|
|
20
|
+
* // Monthly on 1st and 15th at 0:00
|
|
21
|
+
* buildCron({ type: 'monthly', hour: 0, minute: 0, monthDays: [1, 15], ... }) // '0 0 1,15 * *'
|
|
22
|
+
*/
|
|
23
|
+
export function buildCron(state: CronSchedulerState): string {
|
|
24
|
+
const { type, hour, minute, weekDays, monthDays, customCron } = state;
|
|
25
|
+
|
|
26
|
+
// Ensure valid hour/minute
|
|
27
|
+
const h = Math.max(0, Math.min(23, hour));
|
|
28
|
+
const m = Math.max(0, Math.min(59, minute));
|
|
29
|
+
|
|
30
|
+
switch (type) {
|
|
31
|
+
case 'daily':
|
|
32
|
+
// Run every day at specified time
|
|
33
|
+
return `${m} ${h} * * *`;
|
|
34
|
+
|
|
35
|
+
case 'weekly': {
|
|
36
|
+
// Run on specified days of week at specified time
|
|
37
|
+
const sortedDays = [...weekDays].sort((a, b) => a - b);
|
|
38
|
+
const daysStr = sortedDays.length > 0 ? formatNumberList(sortedDays) : '*';
|
|
39
|
+
return `${m} ${h} * * ${daysStr}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case 'monthly': {
|
|
43
|
+
// Run on specified days of month at specified time
|
|
44
|
+
const sortedDays = [...monthDays].sort((a, b) => a - b);
|
|
45
|
+
const daysStr = sortedDays.length > 0 ? formatNumberList(sortedDays) : '1';
|
|
46
|
+
return `${m} ${h} ${daysStr} * *`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'custom':
|
|
50
|
+
// Return user-provided expression
|
|
51
|
+
return customCron.trim() || '* * * * *';
|
|
52
|
+
|
|
53
|
+
default:
|
|
54
|
+
return '0 0 * * *';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Formats a list of numbers as ranges where possible
|
|
60
|
+
* [1,2,3,5,7,8,9] -> "1-3,5,7-9"
|
|
61
|
+
*/
|
|
62
|
+
function formatNumberList(nums: number[]): string {
|
|
63
|
+
if (nums.length === 0) return '';
|
|
64
|
+
if (nums.length === 1) return nums[0].toString();
|
|
65
|
+
|
|
66
|
+
const sorted = [...nums].sort((a, b) => a - b);
|
|
67
|
+
const ranges: string[] = [];
|
|
68
|
+
let start = sorted[0];
|
|
69
|
+
let end = sorted[0];
|
|
70
|
+
|
|
71
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
72
|
+
if (sorted[i] === end + 1) {
|
|
73
|
+
// Continue the range
|
|
74
|
+
end = sorted[i];
|
|
75
|
+
} else {
|
|
76
|
+
// End current range, start new one
|
|
77
|
+
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
78
|
+
start = sorted[i];
|
|
79
|
+
end = sorted[i];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Don't forget the last range
|
|
84
|
+
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
85
|
+
|
|
86
|
+
return ranges.join(',');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds cron for a specific schedule type with defaults
|
|
91
|
+
*/
|
|
92
|
+
export function buildDefaultCron(type: CronSchedulerState['type']): string {
|
|
93
|
+
const defaults: Record<typeof type, string> = {
|
|
94
|
+
daily: '0 9 * * *', // Every day at 9:00
|
|
95
|
+
weekly: '0 9 * * 1-5', // Weekdays at 9:00
|
|
96
|
+
monthly: '0 9 1 * *', // 1st of month at 9:00
|
|
97
|
+
custom: '* * * * *', // Every minute
|
|
98
|
+
};
|
|
99
|
+
return defaults[type];
|
|
100
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron Humanize
|
|
3
|
+
*
|
|
4
|
+
* Converts cron expressions to human-readable descriptions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
8
|
+
const WEEKDAY_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converts cron expression to human-readable description
|
|
12
|
+
*
|
|
13
|
+
* Examples:
|
|
14
|
+
* - humanizeCron('0 9 * * *') returns "Every day at 09:00"
|
|
15
|
+
* - humanizeCron('30 14 * * 1,3,5') returns "Mon, Wed, Fri at 14:30"
|
|
16
|
+
* - humanizeCron('0 0 1 * *') returns "1st of every month at 00:00"
|
|
17
|
+
*/
|
|
18
|
+
export function humanizeCron(cron: string): string {
|
|
19
|
+
if (!cron || typeof cron !== 'string') return 'Invalid schedule';
|
|
20
|
+
|
|
21
|
+
const parts = cron.trim().split(/\s+/);
|
|
22
|
+
if (parts.length !== 5) return 'Invalid schedule';
|
|
23
|
+
|
|
24
|
+
const [minutePart, hourPart, dayOfMonthPart, monthPart, dayOfWeekPart] = parts;
|
|
25
|
+
|
|
26
|
+
// Parse time
|
|
27
|
+
const minute = parseInt(minutePart, 10);
|
|
28
|
+
const hour = parseInt(hourPart, 10);
|
|
29
|
+
const timeStr = formatTime(hour, minute);
|
|
30
|
+
|
|
31
|
+
// Every minute
|
|
32
|
+
if (minutePart === '*' && hourPart === '*' && dayOfMonthPart === '*' && monthPart === '*' && dayOfWeekPart === '*') {
|
|
33
|
+
return 'Every minute';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Every N minutes
|
|
37
|
+
if (minutePart.startsWith('*/')) {
|
|
38
|
+
const interval = parseInt(minutePart.slice(2), 10);
|
|
39
|
+
if (hourPart === '*') {
|
|
40
|
+
return `Every ${interval} minute${interval > 1 ? 's' : ''}`;
|
|
41
|
+
}
|
|
42
|
+
return `Every ${interval} minute${interval > 1 ? 's' : ''} at hour ${hour}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Hourly
|
|
46
|
+
if (!isNaN(minute) && hourPart === '*' && dayOfMonthPart === '*' && monthPart === '*' && dayOfWeekPart === '*') {
|
|
47
|
+
return `Every hour at minute ${minute}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Daily
|
|
51
|
+
if (dayOfMonthPart === '*' && monthPart === '*' && dayOfWeekPart === '*') {
|
|
52
|
+
return `Every day at ${timeStr}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Weekly
|
|
56
|
+
if (dayOfMonthPart === '*' && monthPart === '*' && dayOfWeekPart !== '*') {
|
|
57
|
+
const days = parseWeekDays(dayOfWeekPart);
|
|
58
|
+
|
|
59
|
+
if (days.length === 7) {
|
|
60
|
+
return `Every day at ${timeStr}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (days.length === 5 && isWeekdays(days)) {
|
|
64
|
+
return `Weekdays at ${timeStr}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (days.length === 2 && isWeekend(days)) {
|
|
68
|
+
return `Weekends at ${timeStr}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const dayNames = days.map(d => WEEKDAY_SHORT[d]).join(', ');
|
|
72
|
+
return `${dayNames} at ${timeStr}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Monthly
|
|
76
|
+
if (dayOfMonthPart !== '*' && monthPart === '*' && dayOfWeekPart === '*') {
|
|
77
|
+
const days = parseMonthDays(dayOfMonthPart);
|
|
78
|
+
|
|
79
|
+
if (days.length === 1) {
|
|
80
|
+
return `${ordinal(days[0])} of every month at ${timeStr}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (days.length <= 3) {
|
|
84
|
+
const dayStr = days.map(d => ordinal(d)).join(', ');
|
|
85
|
+
return `${dayStr} of every month at ${timeStr}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return `${days.length} days per month at ${timeStr}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Complex expression - show as-is
|
|
92
|
+
return `Custom schedule`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Formats time as HH:MM
|
|
97
|
+
*/
|
|
98
|
+
function formatTime(hour: number, minute: number): string {
|
|
99
|
+
const h = isNaN(hour) ? 0 : Math.max(0, Math.min(23, hour));
|
|
100
|
+
const m = isNaN(minute) ? 0 : Math.max(0, Math.min(59, minute));
|
|
101
|
+
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Formats time in 12-hour format with AM/PM
|
|
106
|
+
*/
|
|
107
|
+
export function formatTime12h(hour: number, minute: number): string {
|
|
108
|
+
const h = isNaN(hour) ? 0 : Math.max(0, Math.min(23, hour));
|
|
109
|
+
const m = isNaN(minute) ? 0 : Math.max(0, Math.min(59, minute));
|
|
110
|
+
const period = h >= 12 ? 'PM' : 'AM';
|
|
111
|
+
const h12 = h % 12 || 12;
|
|
112
|
+
return `${h12}:${m.toString().padStart(2, '0')} ${period}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parses week days from cron field
|
|
117
|
+
*/
|
|
118
|
+
function parseWeekDays(part: string): number[] {
|
|
119
|
+
const result: number[] = [];
|
|
120
|
+
|
|
121
|
+
// Handle ranges like 1-5
|
|
122
|
+
if (part.includes('-') && !part.includes(',')) {
|
|
123
|
+
const [start, end] = part.split('-').map(s => parseInt(s, 10));
|
|
124
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
125
|
+
for (let i = start; i <= end && i <= 6; i++) {
|
|
126
|
+
if (i >= 0) result.push(i);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle comma-separated list
|
|
133
|
+
for (const segment of part.split(',')) {
|
|
134
|
+
// Handle nested ranges
|
|
135
|
+
if (segment.includes('-')) {
|
|
136
|
+
const [start, end] = segment.split('-').map(s => parseInt(s, 10));
|
|
137
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
138
|
+
for (let i = start; i <= end && i <= 6; i++) {
|
|
139
|
+
if (i >= 0) result.push(i);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
const num = parseInt(segment.trim(), 10);
|
|
144
|
+
if (!isNaN(num) && num >= 0 && num <= 6) {
|
|
145
|
+
result.push(num);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result.sort((a, b) => a - b);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parses month days from cron field
|
|
155
|
+
*/
|
|
156
|
+
function parseMonthDays(part: string): number[] {
|
|
157
|
+
const result: number[] = [];
|
|
158
|
+
|
|
159
|
+
// Handle ranges
|
|
160
|
+
if (part.includes('-') && !part.includes(',')) {
|
|
161
|
+
const [start, end] = part.split('-').map(s => parseInt(s, 10));
|
|
162
|
+
if (!isNaN(start) && !isNaN(end)) {
|
|
163
|
+
for (let i = start; i <= end && i <= 31; i++) {
|
|
164
|
+
if (i >= 1) result.push(i);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle comma-separated list
|
|
171
|
+
for (const segment of part.split(',')) {
|
|
172
|
+
const num = parseInt(segment.trim(), 10);
|
|
173
|
+
if (!isNaN(num) && num >= 1 && num <= 31) {
|
|
174
|
+
result.push(num);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result.sort((a, b) => a - b);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Checks if days array represents weekdays (Mon-Fri)
|
|
183
|
+
*/
|
|
184
|
+
function isWeekdays(days: number[]): boolean {
|
|
185
|
+
const sorted = [...days].sort((a, b) => a - b);
|
|
186
|
+
return sorted.join(',') === '1,2,3,4,5';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Checks if days array represents weekend (Sat-Sun)
|
|
191
|
+
*/
|
|
192
|
+
function isWeekend(days: number[]): boolean {
|
|
193
|
+
const sorted = [...days].sort((a, b) => a - b);
|
|
194
|
+
return sorted.join(',') === '0,6';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns ordinal suffix for a number (1st, 2nd, 3rd, etc.)
|
|
199
|
+
*/
|
|
200
|
+
function ordinal(n: number): string {
|
|
201
|
+
const s = ['th', 'st', 'nd', 'rd'];
|
|
202
|
+
const v = n % 100;
|
|
203
|
+
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Gets full weekday name
|
|
208
|
+
*/
|
|
209
|
+
export function getWeekdayName(day: number): string {
|
|
210
|
+
return WEEKDAY_NAMES[day] || 'Unknown';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Gets short weekday name
|
|
215
|
+
*/
|
|
216
|
+
export function getWeekdayShort(day: number): string {
|
|
217
|
+
return WEEKDAY_SHORT[day] || '?';
|
|
218
|
+
}
|