@djangocfg/ui-tools 2.1.129 → 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,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CronScheduler Client Component
|
|
5
|
+
*
|
|
6
|
+
* Compact cron expression builder following Apple HIG principles.
|
|
7
|
+
* Uses context-based architecture for state management.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
11
|
+
import { CronSchedulerProvider } from './context/CronSchedulerContext';
|
|
12
|
+
import { useCronType } from './context/hooks';
|
|
13
|
+
import {
|
|
14
|
+
ScheduleTypeSelector,
|
|
15
|
+
TimeSelector,
|
|
16
|
+
DayChips,
|
|
17
|
+
MonthDayGrid,
|
|
18
|
+
CustomInput,
|
|
19
|
+
SchedulePreview,
|
|
20
|
+
} from './components';
|
|
21
|
+
import type { CronSchedulerProps } from './types';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Inner Component (uses context)
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
interface CronSchedulerInnerProps {
|
|
28
|
+
showPreview: boolean;
|
|
29
|
+
showCronExpression: boolean;
|
|
30
|
+
allowCopy: boolean;
|
|
31
|
+
timeFormat: '12h' | '24h';
|
|
32
|
+
disabled: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CronSchedulerInner({
|
|
37
|
+
showPreview,
|
|
38
|
+
showCronExpression,
|
|
39
|
+
allowCopy,
|
|
40
|
+
timeFormat,
|
|
41
|
+
disabled,
|
|
42
|
+
className,
|
|
43
|
+
}: CronSchedulerInnerProps) {
|
|
44
|
+
const { type } = useCronType();
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={cn('space-y-3', className)}>
|
|
48
|
+
{/* Schedule Type Selector */}
|
|
49
|
+
<ScheduleTypeSelector disabled={disabled} />
|
|
50
|
+
|
|
51
|
+
{/* Time Selector (shown for daily, weekly, monthly) */}
|
|
52
|
+
{type !== 'custom' && (
|
|
53
|
+
<TimeSelector format={timeFormat} disabled={disabled} />
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Day Chips (weekly only) */}
|
|
57
|
+
{type === 'weekly' && (
|
|
58
|
+
<DayChips disabled={disabled} showPresets />
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{/* Month Day Grid (monthly only) */}
|
|
62
|
+
{type === 'monthly' && (
|
|
63
|
+
<MonthDayGrid disabled={disabled} showPresets />
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{/* Custom Input (custom only) */}
|
|
67
|
+
{type === 'custom' && (
|
|
68
|
+
<CustomInput disabled={disabled} />
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{/* Preview - always show cron expression */}
|
|
72
|
+
{showPreview && (
|
|
73
|
+
<SchedulePreview
|
|
74
|
+
showCronExpression
|
|
75
|
+
allowCopy={allowCopy}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Main Component (with Provider)
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* CronScheduler - Compact cron expression builder
|
|
88
|
+
*
|
|
89
|
+
* A user-friendly interface for creating cron schedules without
|
|
90
|
+
* needing to know cron syntax. Follows Apple HIG design principles.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Basic usage
|
|
94
|
+
* <CronScheduler
|
|
95
|
+
* value={cronExpression}
|
|
96
|
+
* onChange={setCronExpression}
|
|
97
|
+
* />
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* // With all options
|
|
101
|
+
* <CronScheduler
|
|
102
|
+
* value="0 9 * * 1-5"
|
|
103
|
+
* onChange={handleChange}
|
|
104
|
+
* defaultType="weekly"
|
|
105
|
+
* showPreview
|
|
106
|
+
* showCronExpression
|
|
107
|
+
* allowCopy
|
|
108
|
+
* timeFormat="24h"
|
|
109
|
+
* />
|
|
110
|
+
*/
|
|
111
|
+
export function CronScheduler({
|
|
112
|
+
value,
|
|
113
|
+
onChange,
|
|
114
|
+
defaultType = 'daily',
|
|
115
|
+
showPreview = true,
|
|
116
|
+
showCronExpression = false,
|
|
117
|
+
allowCopy = false,
|
|
118
|
+
timeFormat = '24h',
|
|
119
|
+
disabled = false,
|
|
120
|
+
className,
|
|
121
|
+
}: CronSchedulerProps) {
|
|
122
|
+
return (
|
|
123
|
+
<CronSchedulerProvider
|
|
124
|
+
value={value}
|
|
125
|
+
onChange={onChange}
|
|
126
|
+
defaultType={defaultType}
|
|
127
|
+
>
|
|
128
|
+
<CronSchedulerInner
|
|
129
|
+
showPreview={showPreview}
|
|
130
|
+
showCronExpression={showCronExpression}
|
|
131
|
+
allowCopy={allowCopy}
|
|
132
|
+
timeFormat={timeFormat}
|
|
133
|
+
disabled={disabled}
|
|
134
|
+
className={className}
|
|
135
|
+
/>
|
|
136
|
+
</CronSchedulerProvider>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default CronScheduler;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { defineStory, useBoolean, useSelect } from '@djangocfg/playground';
|
|
3
|
+
import { CronScheduler } from './index';
|
|
4
|
+
|
|
5
|
+
export default defineStory({
|
|
6
|
+
title: 'Tools/Cron Scheduler',
|
|
7
|
+
component: CronScheduler,
|
|
8
|
+
description: 'Compact cron expression builder with Apple HIG-style UI.',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const Interactive = () => {
|
|
12
|
+
const [cron, setCron] = useState('0 9 * * 1-5');
|
|
13
|
+
|
|
14
|
+
const [defaultType] = useSelect('defaultType', {
|
|
15
|
+
options: ['daily', 'weekly', 'monthly', 'custom'] as const,
|
|
16
|
+
defaultValue: 'weekly',
|
|
17
|
+
label: 'Default Type',
|
|
18
|
+
description: 'Initial schedule type',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const [showPreview] = useBoolean('showPreview', {
|
|
22
|
+
defaultValue: true,
|
|
23
|
+
label: 'Show Preview',
|
|
24
|
+
description: 'Show human-readable preview',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const [showCronExpression] = useBoolean('showCronExpression', {
|
|
28
|
+
defaultValue: true,
|
|
29
|
+
label: 'Show Cron Expression',
|
|
30
|
+
description: 'Show raw cron expression in preview',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const [allowCopy] = useBoolean('allowCopy', {
|
|
34
|
+
defaultValue: true,
|
|
35
|
+
label: 'Allow Copy',
|
|
36
|
+
description: 'Enable copy to clipboard',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const [timeFormat] = useSelect('timeFormat', {
|
|
40
|
+
options: ['24h', '12h'] as const,
|
|
41
|
+
defaultValue: '24h',
|
|
42
|
+
label: 'Time Format',
|
|
43
|
+
description: 'Time display format',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const [disabled] = useBoolean('disabled', {
|
|
47
|
+
defaultValue: false,
|
|
48
|
+
label: 'Disabled',
|
|
49
|
+
description: 'Disable all interactions',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="w-[400px]">
|
|
54
|
+
<CronScheduler
|
|
55
|
+
value={cron}
|
|
56
|
+
onChange={setCron}
|
|
57
|
+
defaultType={defaultType}
|
|
58
|
+
showPreview={showPreview}
|
|
59
|
+
showCronExpression={showCronExpression}
|
|
60
|
+
allowCopy={allowCopy}
|
|
61
|
+
timeFormat={timeFormat}
|
|
62
|
+
disabled={disabled}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const Daily = () => {
|
|
69
|
+
const [cron, setCron] = useState('0 9 * * *');
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="w-[400px]">
|
|
73
|
+
<CronScheduler
|
|
74
|
+
value={cron}
|
|
75
|
+
onChange={setCron}
|
|
76
|
+
defaultType="daily"
|
|
77
|
+
showPreview
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Weekly = () => {
|
|
84
|
+
const [cron, setCron] = useState('30 14 * * 1,3,5');
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="w-[400px]">
|
|
88
|
+
<CronScheduler
|
|
89
|
+
value={cron}
|
|
90
|
+
onChange={setCron}
|
|
91
|
+
defaultType="weekly"
|
|
92
|
+
showPreview
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const Monthly = () => {
|
|
99
|
+
const [cron, setCron] = useState('0 9 1,15 * *');
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="w-[400px]">
|
|
103
|
+
<CronScheduler
|
|
104
|
+
value={cron}
|
|
105
|
+
onChange={setCron}
|
|
106
|
+
defaultType="monthly"
|
|
107
|
+
showPreview
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const Custom = () => {
|
|
114
|
+
const [cron, setCron] = useState('*/15 9-17 * * 1-5');
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="w-[400px]">
|
|
118
|
+
<CronScheduler
|
|
119
|
+
value={cron}
|
|
120
|
+
onChange={setCron}
|
|
121
|
+
defaultType="custom"
|
|
122
|
+
showPreview
|
|
123
|
+
showCronExpression
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const WithCronExpression = () => {
|
|
130
|
+
const [cron, setCron] = useState('0 9 * * 1-5');
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="w-[400px]">
|
|
134
|
+
<CronScheduler
|
|
135
|
+
value={cron}
|
|
136
|
+
onChange={setCron}
|
|
137
|
+
showPreview
|
|
138
|
+
showCronExpression
|
|
139
|
+
allowCopy
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const TwelveHourFormat = () => {
|
|
146
|
+
const [cron, setCron] = useState('0 14 * * *');
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className="w-[400px]">
|
|
150
|
+
<CronScheduler
|
|
151
|
+
value={cron}
|
|
152
|
+
onChange={setCron}
|
|
153
|
+
showPreview
|
|
154
|
+
timeFormat="12h"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const Disabled = () => (
|
|
161
|
+
<div className="w-[400px]">
|
|
162
|
+
<CronScheduler
|
|
163
|
+
value="0 9 * * 1-5"
|
|
164
|
+
showPreview
|
|
165
|
+
disabled
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
export const Compact = () => {
|
|
171
|
+
const [cron, setCron] = useState('0 9 * * 1-5');
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div className="w-[400px]">
|
|
175
|
+
<CronScheduler
|
|
176
|
+
value={cron}
|
|
177
|
+
onChange={setCron}
|
|
178
|
+
defaultType="weekly"
|
|
179
|
+
showPreview={false}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const Controlled = () => {
|
|
186
|
+
const [cron, setCron] = useState('0 9 * * *');
|
|
187
|
+
|
|
188
|
+
const presets = [
|
|
189
|
+
{ label: 'Every day at 9am', value: '0 9 * * *' },
|
|
190
|
+
{ label: 'Weekdays at 6pm', value: '0 18 * * 1-5' },
|
|
191
|
+
{ label: 'Every Monday', value: '0 0 * * 1' },
|
|
192
|
+
{ label: '1st of month', value: '0 0 1 * *' },
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="w-[400px] space-y-4">
|
|
197
|
+
<div className="flex flex-wrap gap-2">
|
|
198
|
+
{presets.map(({ label, value }) => (
|
|
199
|
+
<button
|
|
200
|
+
key={value}
|
|
201
|
+
onClick={() => setCron(value)}
|
|
202
|
+
className={`px-2 py-1 text-xs rounded-md transition-colors ${
|
|
203
|
+
cron === value
|
|
204
|
+
? 'bg-primary text-primary-foreground'
|
|
205
|
+
: 'bg-muted hover:bg-muted/80'
|
|
206
|
+
}`}
|
|
207
|
+
>
|
|
208
|
+
{label}
|
|
209
|
+
</button>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
<CronScheduler
|
|
213
|
+
value={cron}
|
|
214
|
+
onChange={setCron}
|
|
215
|
+
showPreview
|
|
216
|
+
allowCopy
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CronCheatsheet
|
|
5
|
+
*
|
|
6
|
+
* Popover with cron syntax reference.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Popover,
|
|
11
|
+
PopoverTrigger,
|
|
12
|
+
PopoverContent,
|
|
13
|
+
} from '@djangocfg/ui-core/components';
|
|
14
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
15
|
+
import { HelpCircle } from 'lucide-react';
|
|
16
|
+
|
|
17
|
+
export interface CronCheatsheetProps {
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CronCheatsheet({ className }: CronCheatsheetProps) {
|
|
22
|
+
return (
|
|
23
|
+
<Popover>
|
|
24
|
+
<PopoverTrigger asChild>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
className={cn(
|
|
28
|
+
'p-1 rounded hover:bg-muted/50 transition-colors',
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
aria-label="Cron syntax help"
|
|
32
|
+
>
|
|
33
|
+
<HelpCircle className="h-4 w-4 text-muted-foreground" />
|
|
34
|
+
</button>
|
|
35
|
+
</PopoverTrigger>
|
|
36
|
+
<PopoverContent className="w-72 p-3" align="end">
|
|
37
|
+
<div className="space-y-3">
|
|
38
|
+
<p className="font-medium text-sm">Cron Format</p>
|
|
39
|
+
<code className="block text-xs bg-muted px-2 py-1.5 rounded font-mono text-center">
|
|
40
|
+
min hour day month weekday
|
|
41
|
+
</code>
|
|
42
|
+
|
|
43
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
|
44
|
+
<div className="flex justify-between">
|
|
45
|
+
<span className="text-muted-foreground">minute</span>
|
|
46
|
+
<span>0-59</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex justify-between">
|
|
49
|
+
<span className="text-muted-foreground">hour</span>
|
|
50
|
+
<span>0-23</span>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex justify-between">
|
|
53
|
+
<span className="text-muted-foreground">day</span>
|
|
54
|
+
<span>1-31</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex justify-between">
|
|
57
|
+
<span className="text-muted-foreground">month</span>
|
|
58
|
+
<span>1-12</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex justify-between col-span-2">
|
|
61
|
+
<span className="text-muted-foreground">weekday</span>
|
|
62
|
+
<span>0-6 (Sun-Sat)</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="pt-2 border-t border-border space-y-1.5">
|
|
67
|
+
<p className="text-xs font-medium">Special characters</p>
|
|
68
|
+
<div className="grid grid-cols-2 gap-1.5 text-xs">
|
|
69
|
+
<span><code className="bg-muted px-1 rounded">*</code> any value</span>
|
|
70
|
+
<span><code className="bg-muted px-1 rounded">,</code> list (1,3,5)</span>
|
|
71
|
+
<span><code className="bg-muted px-1 rounded">-</code> range (1-5)</span>
|
|
72
|
+
<span><code className="bg-muted px-1 rounded">/</code> step (*/15)</span>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="pt-2 border-t border-border space-y-1.5">
|
|
77
|
+
<p className="text-xs font-medium">Examples</p>
|
|
78
|
+
<div className="space-y-1 text-xs">
|
|
79
|
+
<div className="flex justify-between font-mono">
|
|
80
|
+
<span className="text-muted-foreground">0 9 * * *</span>
|
|
81
|
+
<span>daily at 9am</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="flex justify-between font-mono">
|
|
84
|
+
<span className="text-muted-foreground">0 9 * * 1-5</span>
|
|
85
|
+
<span>weekdays 9am</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="flex justify-between font-mono">
|
|
88
|
+
<span className="text-muted-foreground">*/15 * * * *</span>
|
|
89
|
+
<span>every 15 min</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex justify-between font-mono">
|
|
92
|
+
<span className="text-muted-foreground">0 0 1 * *</span>
|
|
93
|
+
<span>1st of month</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</PopoverContent>
|
|
99
|
+
</Popover>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CustomInput
|
|
5
|
+
*
|
|
6
|
+
* Raw cron expression input with validation and help popover.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect } from 'react';
|
|
10
|
+
import { Input } from '@djangocfg/ui-core/components';
|
|
11
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
12
|
+
import { AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
13
|
+
import { useCronCustom } from '../context/hooks';
|
|
14
|
+
import { isValidCron } from '../utils/cron-parser';
|
|
15
|
+
|
|
16
|
+
export interface CustomInputProps {
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CustomInput({ disabled, className }: CustomInputProps) {
|
|
22
|
+
const { customCron, isValid, setCustomCron } = useCronCustom();
|
|
23
|
+
const [localValue, setLocalValue] = useState(customCron);
|
|
24
|
+
const [localValid, setLocalValid] = useState(isValid);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setLocalValue(customCron);
|
|
28
|
+
setLocalValid(isValid);
|
|
29
|
+
}, [customCron, isValid]);
|
|
30
|
+
|
|
31
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
32
|
+
const value = e.target.value;
|
|
33
|
+
setLocalValue(value);
|
|
34
|
+
const valid = isValidCron(value);
|
|
35
|
+
setLocalValid(valid);
|
|
36
|
+
setCustomCron(value);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn('space-y-2', className)}>
|
|
41
|
+
<span className="text-sm text-muted-foreground">Cron expression</span>
|
|
42
|
+
|
|
43
|
+
<div className="relative">
|
|
44
|
+
<Input
|
|
45
|
+
type="text"
|
|
46
|
+
value={localValue}
|
|
47
|
+
onChange={handleChange}
|
|
48
|
+
disabled={disabled}
|
|
49
|
+
placeholder="* * * * *"
|
|
50
|
+
className={cn(
|
|
51
|
+
'font-mono text-base pr-10 h-11',
|
|
52
|
+
!localValid && localValue.trim() && 'border-destructive focus-visible:ring-destructive/50'
|
|
53
|
+
)}
|
|
54
|
+
/>
|
|
55
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
56
|
+
{localValue.trim() && (
|
|
57
|
+
localValid ? (
|
|
58
|
+
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
|
59
|
+
) : (
|
|
60
|
+
<AlertCircle className="h-5 w-5 text-destructive" />
|
|
61
|
+
)
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DayChips
|
|
5
|
+
*
|
|
6
|
+
* Full-width day of week selector with clear labels.
|
|
7
|
+
* Shows abbreviated day names for clarity.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
11
|
+
import { useCronWeekDays } from '../context/hooks';
|
|
12
|
+
import type { WeekDay } from '../types';
|
|
13
|
+
|
|
14
|
+
const DAYS: { value: WeekDay; label: string }[] = [
|
|
15
|
+
{ value: 1, label: 'Mon' },
|
|
16
|
+
{ value: 2, label: 'Tue' },
|
|
17
|
+
{ value: 3, label: 'Wed' },
|
|
18
|
+
{ value: 4, label: 'Thu' },
|
|
19
|
+
{ value: 5, label: 'Fri' },
|
|
20
|
+
{ value: 6, label: 'Sat' },
|
|
21
|
+
{ value: 0, label: 'Sun' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export interface DayChipsProps {
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
showPresets?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function DayChips({
|
|
31
|
+
disabled,
|
|
32
|
+
showPresets = true,
|
|
33
|
+
className,
|
|
34
|
+
}: DayChipsProps) {
|
|
35
|
+
const { weekDays, toggleWeekDay, setWeekDays } = useCronWeekDays();
|
|
36
|
+
|
|
37
|
+
const isWeekdays = weekDays.length === 5 && [1,2,3,4,5].every(d => weekDays.includes(d as WeekDay));
|
|
38
|
+
const isWeekend = weekDays.length === 2 && [0,6].every(d => weekDays.includes(d as WeekDay));
|
|
39
|
+
const isEveryday = weekDays.length === 7;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn('space-y-3', className)}>
|
|
43
|
+
{/* Day Grid - full width */}
|
|
44
|
+
<div className="grid grid-cols-7 gap-1">
|
|
45
|
+
{DAYS.map(({ value, label }) => {
|
|
46
|
+
const isSelected = weekDays.includes(value);
|
|
47
|
+
const isWeekend = value === 0 || value === 6;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<button
|
|
51
|
+
key={value}
|
|
52
|
+
type="button"
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
onClick={() => toggleWeekDay(value)}
|
|
55
|
+
aria-pressed={isSelected}
|
|
56
|
+
className={cn(
|
|
57
|
+
'flex flex-col items-center justify-center',
|
|
58
|
+
'py-2.5 rounded-lg text-xs font-medium',
|
|
59
|
+
'transition-all duration-150',
|
|
60
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
61
|
+
'active:scale-[0.97]',
|
|
62
|
+
isSelected
|
|
63
|
+
? 'bg-primary text-primary-foreground shadow-sm'
|
|
64
|
+
: cn(
|
|
65
|
+
'bg-muted/50 hover:bg-muted',
|
|
66
|
+
isWeekend ? 'text-muted-foreground/70' : 'text-muted-foreground'
|
|
67
|
+
),
|
|
68
|
+
disabled && 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<span>{label}</span>
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Quick Presets */}
|
|
78
|
+
{showPresets && (
|
|
79
|
+
<div className="flex gap-2">
|
|
80
|
+
<PresetButton
|
|
81
|
+
label="Weekdays"
|
|
82
|
+
isActive={isWeekdays}
|
|
83
|
+
onClick={() => setWeekDays([1, 2, 3, 4, 5] as WeekDay[])}
|
|
84
|
+
disabled={disabled}
|
|
85
|
+
/>
|
|
86
|
+
<PresetButton
|
|
87
|
+
label="Weekends"
|
|
88
|
+
isActive={isWeekend}
|
|
89
|
+
onClick={() => setWeekDays([0, 6] as WeekDay[])}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
/>
|
|
92
|
+
<PresetButton
|
|
93
|
+
label="Every day"
|
|
94
|
+
isActive={isEveryday}
|
|
95
|
+
onClick={() => setWeekDays([0, 1, 2, 3, 4, 5, 6] as WeekDay[])}
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface PresetButtonProps {
|
|
105
|
+
label: string;
|
|
106
|
+
isActive: boolean;
|
|
107
|
+
onClick: () => void;
|
|
108
|
+
disabled?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function PresetButton({ label, isActive, onClick, disabled }: PresetButtonProps) {
|
|
112
|
+
return (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
disabled={disabled}
|
|
116
|
+
onClick={onClick}
|
|
117
|
+
className={cn(
|
|
118
|
+
'flex-1 px-3 py-1.5 rounded-md text-xs font-medium',
|
|
119
|
+
'transition-colors duration-150',
|
|
120
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
|
121
|
+
isActive
|
|
122
|
+
? 'bg-primary/15 text-primary border border-primary/30'
|
|
123
|
+
: 'bg-muted/30 text-muted-foreground hover:bg-muted/50 border border-transparent',
|
|
124
|
+
disabled && 'opacity-50 cursor-not-allowed'
|
|
125
|
+
)}
|
|
126
|
+
>
|
|
127
|
+
{label}
|
|
128
|
+
</button>
|
|
129
|
+
);
|
|
130
|
+
}
|