feat(recurrence): Daily tab is "every day at <time>" — drop weekday choice

The Daily tab had two radios: "Every day" vs "Every weekday (Mon–Fri)".
That's confusing — Mon-Fri-only is a weekly pattern, not a daily one,
and it overlapped exactly with what the Weekly tab can already do
(select Mon, Tue, Wed, Thu, Fri).

So Daily now means literally every day. The tab body is just the time
picker plus a one-liner ("Fires once a day at the time below.").

Legacy reminders that stored "MM HH * * 1-5" still load fine — the
parser maps any DOW list (including the 1-5 range) onto a Weekly draft
with Mon-Fri pre-selected. So a saved "weekday" daily reminder shows
up as Weekly with the right days checked, no data loss.

RadioRow component went unused after this — removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 11:21:41 +08:00
parent 08435988c2
commit 657fa71bf9

View File

@ -34,7 +34,6 @@ type RuleType = "daily" | "weekly" | "monthly" | "yearly";
interface Draft { interface Draft {
type: RuleType; type: RuleType;
dailyMode: "every_day" | "weekdays";
/** Cron weekday list (0=Sun..6=Sat). */ /** Cron weekday list (0=Sun..6=Sat). */
weekdays: number[]; weekdays: number[];
monthDay: number; monthDay: number;
@ -50,7 +49,6 @@ const MAX_RULES = 8;
function defaultDraft(firstFire: DateTime): Draft { function defaultDraft(firstFire: DateTime): Draft {
return { return {
type: "daily", type: "daily",
dailyMode: "every_day",
weekdays: [isoWeekdayToCron(firstFire.weekday)], weekdays: [isoWeekdayToCron(firstFire.weekday)],
monthDay: firstFire.day, monthDay: firstFire.day,
month: firstFire.month, month: firstFire.month,
@ -87,7 +85,7 @@ function draftToCron(d: Draft): string | null {
const h = clamp(d.hour, 0, 23); const h = clamp(d.hour, 0, 23);
switch (d.type) { switch (d.type) {
case "daily": case "daily":
return d.dailyMode === "weekdays" ? `${m} ${h} * * 1-5` : `${m} ${h} * * *`; return `${m} ${h} * * *`;
case "weekly": case "weekly":
if (!d.weekdays.length) return null; if (!d.weekdays.length) return null;
return `${m} ${h} * * ${d.weekdays.slice().sort((a, b) => a - b).join(",")}`; return `${m} ${h} * * ${d.weekdays.slice().sort((a, b) => a - b).join(",")}`;
@ -102,9 +100,7 @@ function describeDraft(d: Draft): string {
const t = `${pad2(clamp(d.hour, 0, 23))}:${pad2(clamp(d.minute, 0, 59))}`; const t = `${pad2(clamp(d.hour, 0, 23))}:${pad2(clamp(d.minute, 0, 59))}`;
switch (d.type) { switch (d.type) {
case "daily": case "daily":
return d.dailyMode === "weekdays" return `Every day at ${t}`;
? `Every weekday at ${t}`
: `Every day at ${t}`;
case "weekly": { case "weekly": {
if (!d.weekdays.length) return "Pick at least one weekday"; if (!d.weekdays.length) return "Pick at least one weekday";
const labels = d.weekdays const labels = d.weekdays
@ -137,12 +133,11 @@ function draftFromCronExpr(expr: string, firstFire: DateTime): Draft {
const rest = head[3]!.trim(); const rest = head[3]!.trim();
let m: RegExpMatchArray | null; let m: RegExpMatchArray | null;
if (rest === "* * 1-5") {
return { ...base, type: "daily", dailyMode: "weekdays", hour, minute };
}
if (rest === "* * *") { if (rest === "* * *") {
return { ...base, type: "daily", dailyMode: "every_day", hour, minute }; return { ...base, type: "daily", hour, minute };
} }
// Any DOW list (including the legacy "1-5" weekday-only daily rule)
// round-trips as a Weekly draft.
if ((m = rest.match(/^\* \* ([0-9,\-]+)$/))) { if ((m = rest.match(/^\* \* ([0-9,\-]+)$/))) {
const days = m[1]! const days = m[1]!
.split(",") .split(",")
@ -357,18 +352,9 @@ function RuleEditor({ draft, onChange }: RuleEditorProps) {
</TabsList> </TabsList>
<TabsContent value="daily" className="space-y-3 pt-3"> <TabsContent value="daily" className="space-y-3 pt-3">
<RadioRow <p className="text-xs text-muted-foreground">
name={`daily-${draft.type}`} Fires once a day at the time below.
checked={draft.dailyMode === "every_day"} </p>
onChange={() => onChange({ dailyMode: "every_day" })}
label="Every day"
/>
<RadioRow
name={`daily-${draft.type}`}
checked={draft.dailyMode === "weekdays"}
onChange={() => onChange({ dailyMode: "weekdays" })}
label="Every weekday (Mon Fri)"
/>
<TimeField draft={draft} onChange={onChange} /> <TimeField draft={draft} onChange={onChange} />
</TabsContent> </TabsContent>
@ -485,24 +471,3 @@ function TimeField({ draft, onChange }: TimeFieldProps) {
); );
} }
interface RadioRowProps {
name: string;
checked: boolean;
onChange: () => void;
label: string;
}
function RadioRow({ name, checked, onChange, label }: RadioRowProps) {
return (
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name={name}
checked={checked}
onChange={onChange}
className="size-4 accent-primary"
/>
<span className="text-sm">{label}</span>
</label>
);
}