refactor: Extract reusable UI components and add TDD documentation
- Add comprehensive TDD documentation to CLAUDE.md with coverage requirements and examples - Extract reusable UI components to frontend/src/components/ui/ (Modal, FormInput, Button, Alert, etc.) - Add shared constants (schedulePresets) and utility hooks (useCrudMutation, useFormValidation) - Update frontend/CLAUDE.md with component documentation and usage examples - Refactor CreateTaskModal to use shared components and constants - Fix test assertions to be more robust and accurate across all test files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
211
frontend/src/constants/schedulePresets.ts
Normal file
211
frontend/src/constants/schedulePresets.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Schedule presets for scheduled tasks and event automations.
|
||||
* Shared between CreateTaskModal and EditTaskModal.
|
||||
*/
|
||||
|
||||
export interface SchedulePreset {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
type: 'INTERVAL' | 'CRON';
|
||||
interval_minutes?: number;
|
||||
cron_expression?: string;
|
||||
}
|
||||
|
||||
export const SCHEDULE_PRESETS: SchedulePreset[] = [
|
||||
// Interval-based
|
||||
{
|
||||
id: 'every_15min',
|
||||
label: 'Every 15 minutes',
|
||||
description: 'Runs 4 times per hour',
|
||||
type: 'INTERVAL',
|
||||
interval_minutes: 15,
|
||||
},
|
||||
{
|
||||
id: 'every_30min',
|
||||
label: 'Every 30 minutes',
|
||||
description: 'Runs twice per hour',
|
||||
type: 'INTERVAL',
|
||||
interval_minutes: 30,
|
||||
},
|
||||
{
|
||||
id: 'every_hour',
|
||||
label: 'Every hour',
|
||||
description: 'Runs 24 times per day',
|
||||
type: 'INTERVAL',
|
||||
interval_minutes: 60,
|
||||
},
|
||||
{
|
||||
id: 'every_2hours',
|
||||
label: 'Every 2 hours',
|
||||
description: 'Runs 12 times per day',
|
||||
type: 'INTERVAL',
|
||||
interval_minutes: 120,
|
||||
},
|
||||
{
|
||||
id: 'every_4hours',
|
||||
label: 'Every 4 hours',
|
||||
description: 'Runs 6 times per day',
|
||||
type: 'INTERVAL',
|
||||
interval_minutes: 240,
|
||||
},
|
||||
{
|
||||
id: 'every_6hours',
|
||||
label: 'Every 6 hours',
|
||||
description: 'Runs 4 times per day',
|
||||
type: 'INTERVAL',
|
||||
interval_minutes: 360,
|
||||
},
|
||||
{
|
||||
id: 'every_12hours',
|
||||
label: 'Twice daily',
|
||||
description: 'Runs at midnight and noon',
|
||||
type: 'INTERVAL',
|
||||
interval_minutes: 720,
|
||||
},
|
||||
// Cron-based (specific times)
|
||||
{
|
||||
id: 'daily_midnight',
|
||||
label: 'Daily at midnight',
|
||||
description: 'Runs once per day at 12:00 AM',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 0 * * *',
|
||||
},
|
||||
{
|
||||
id: 'daily_9am',
|
||||
label: 'Daily at 9 AM',
|
||||
description: 'Runs once per day at 9:00 AM',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 9 * * *',
|
||||
},
|
||||
{
|
||||
id: 'daily_6pm',
|
||||
label: 'Daily at 6 PM',
|
||||
description: 'Runs once per day at 6:00 PM',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 18 * * *',
|
||||
},
|
||||
{
|
||||
id: 'weekdays_9am',
|
||||
label: 'Weekdays at 9 AM',
|
||||
description: 'Mon-Fri at 9:00 AM',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 9 * * 1-5',
|
||||
},
|
||||
{
|
||||
id: 'weekdays_6pm',
|
||||
label: 'Weekdays at 6 PM',
|
||||
description: 'Mon-Fri at 6:00 PM',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 18 * * 1-5',
|
||||
},
|
||||
{
|
||||
id: 'weekly_sunday',
|
||||
label: 'Weekly on Sunday',
|
||||
description: 'Every Sunday at midnight',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 0 * * 0',
|
||||
},
|
||||
{
|
||||
id: 'weekly_monday',
|
||||
label: 'Weekly on Monday',
|
||||
description: 'Every Monday at 9:00 AM',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 9 * * 1',
|
||||
},
|
||||
{
|
||||
id: 'monthly_1st',
|
||||
label: 'Monthly on the 1st',
|
||||
description: 'First day of each month',
|
||||
type: 'CRON',
|
||||
cron_expression: '0 0 1 * *',
|
||||
},
|
||||
];
|
||||
|
||||
/** Event trigger options for event automations */
|
||||
export interface TriggerOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const TRIGGER_OPTIONS: TriggerOption[] = [
|
||||
{ value: 'before_start', label: 'Before Start' },
|
||||
{ value: 'at_start', label: 'At Start' },
|
||||
{ value: 'after_start', label: 'After Start' },
|
||||
{ value: 'after_end', label: 'After End' },
|
||||
{ value: 'on_complete', label: 'When Completed' },
|
||||
{ value: 'on_cancel', label: 'When Canceled' },
|
||||
];
|
||||
|
||||
/** Offset presets for event automations */
|
||||
export interface OffsetPreset {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const OFFSET_PRESETS: OffsetPreset[] = [
|
||||
{ value: 0, label: 'Immediately' },
|
||||
{ value: 5, label: '5 min' },
|
||||
{ value: 10, label: '10 min' },
|
||||
{ value: 15, label: '15 min' },
|
||||
{ value: 30, label: '30 min' },
|
||||
{ value: 60, label: '1 hour' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a schedule preset by ID
|
||||
*/
|
||||
export const getSchedulePreset = (id: string): SchedulePreset | undefined => {
|
||||
return SCHEDULE_PRESETS.find((preset) => preset.id === id);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get schedule description for display
|
||||
*/
|
||||
export const getScheduleDescription = (
|
||||
scheduleMode: 'preset' | 'onetime' | 'advanced',
|
||||
selectedPreset: string,
|
||||
runAtDate?: string,
|
||||
runAtTime?: string,
|
||||
customCron?: string
|
||||
): string => {
|
||||
if (scheduleMode === 'onetime') {
|
||||
if (runAtDate && runAtTime) {
|
||||
return `Once on ${new Date(`${runAtDate}T${runAtTime}`).toLocaleString()}`;
|
||||
}
|
||||
return 'Select date and time';
|
||||
}
|
||||
if (scheduleMode === 'advanced') {
|
||||
return `Custom: ${customCron || '0 0 * * *'}`;
|
||||
}
|
||||
const preset = getSchedulePreset(selectedPreset);
|
||||
return preset?.description || 'Select a schedule';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get event timing description for display
|
||||
*/
|
||||
export const getEventTimingDescription = (
|
||||
selectedTrigger: string,
|
||||
selectedOffset: number
|
||||
): string => {
|
||||
const trigger = TRIGGER_OPTIONS.find((t) => t.value === selectedTrigger);
|
||||
if (!trigger) return 'Select timing';
|
||||
|
||||
if (selectedTrigger === 'on_complete') return 'When event is completed';
|
||||
if (selectedTrigger === 'on_cancel') return 'When event is canceled';
|
||||
|
||||
if (selectedOffset === 0) {
|
||||
if (['before_start', 'at_start', 'after_start'].includes(selectedTrigger)) {
|
||||
return 'At event start';
|
||||
}
|
||||
if (selectedTrigger === 'after_end') return 'At event end';
|
||||
}
|
||||
|
||||
const offsetLabel = OFFSET_PRESETS.find((o) => o.value === selectedOffset)?.label || `${selectedOffset} min`;
|
||||
if (selectedTrigger === 'before_start') return `${offsetLabel} before event starts`;
|
||||
if (['at_start', 'after_start'].includes(selectedTrigger)) return `${offsetLabel} after event starts`;
|
||||
if (selectedTrigger === 'after_end') return `${offsetLabel} after event ends`;
|
||||
|
||||
return trigger.label;
|
||||
};
|
||||
Reference in New Issue
Block a user