Resolve an occurrence back to the rule behind it
The detail sheet looked its record up by the row's id. For an occurrence that id carries a #date suffix and is not a key in the store, so `own` came back undefined and the edit and delete buttons vanished on every recurring row — and a save would have reached editEvent with an id it could not find and quietly done nothing. The suffix is deliberate: marks key off the occurrence so each time round carries its own completion. There is still only one record to edit. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4f53b7e153
commit
60041134ee
+16
-12
@@ -19,7 +19,7 @@ import { useProgress } from "./state/useProgress.ts";
|
|||||||
import { useDailyLog, type DailyLogMap } from "./state/useDailyLog.ts";
|
import { useDailyLog, type DailyLogMap } from "./state/useDailyLog.ts";
|
||||||
import { adoptNewLanes, usePrefs } from "./state/usePrefs.ts";
|
import { adoptNewLanes, usePrefs } from "./state/usePrefs.ts";
|
||||||
import { snapDayWidth } from "./state/zoom.ts";
|
import { snapDayWidth } from "./state/zoom.ts";
|
||||||
import { useCustom } from "./state/useCustom.ts";
|
import { useCustom, type EventDraft } from "./state/useCustom.ts";
|
||||||
import { compareRows, SORT_MODES, type Activity, type SortMode } from "./state/sort.ts";
|
import { compareRows, SORT_MODES, type Activity, type SortMode } from "./state/sort.ts";
|
||||||
import {
|
import {
|
||||||
advanceFocus,
|
advanceFocus,
|
||||||
@@ -34,6 +34,7 @@ import { orderGames } from "./state/gameOrder.ts";
|
|||||||
import { GameMetaProvider, type MetaResolver } from "./state/gameMeta.tsx";
|
import { GameMetaProvider, type MetaResolver } from "./state/gameMeta.tsx";
|
||||||
import { metaOnTheme, useTheme } from "./state/theme.ts";
|
import { metaOnTheme, useTheme } from "./state/theme.ts";
|
||||||
import {
|
import {
|
||||||
|
recordFor,
|
||||||
type CustomEvents,
|
type CustomEvents,
|
||||||
type CustomGames,
|
type CustomGames,
|
||||||
type LaneId,
|
type LaneId,
|
||||||
@@ -692,17 +693,20 @@ export function App() {
|
|||||||
onNote={prog.setNote}
|
onNote={prog.setNote}
|
||||||
onIgnore={(id) => toggleIgnored(id, openRow.event.title)}
|
onIgnore={(id) => toggleIgnored(id, openRow.event.title)}
|
||||||
onClose={() => setOpenId(null)}
|
onClose={() => setOpenId(null)}
|
||||||
own={
|
own={(() => {
|
||||||
custom.events[openRow.event.id] === undefined
|
// The row may be one occurrence of a rule. Marks key off the
|
||||||
? undefined
|
// occurrence; the record to edit is the rule behind it.
|
||||||
: {
|
const record = recordFor(custom.events, openRow.event.id);
|
||||||
record: custom.events[openRow.event.id]!,
|
if (record === undefined) return undefined;
|
||||||
lanes: games,
|
return {
|
||||||
games: custom.games,
|
record,
|
||||||
onSave: custom.editEvent,
|
lanes: games,
|
||||||
onDelete: custom.removeEvent,
|
games: custom.games,
|
||||||
}
|
onSave: (_id: string, draft: EventDraft) =>
|
||||||
}
|
custom.editEvent(record.id, draft),
|
||||||
|
onDelete: () => custom.removeEvent(record.id),
|
||||||
|
};
|
||||||
|
})()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Shell>
|
</Shell>
|
||||||
|
|||||||
+19
-1
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { comesRoundEarly, Repeat } from "./recurrence.ts";
|
import { comesRoundEarly, Repeat, ruleIdOf } from "./recurrence.ts";
|
||||||
import type { Occurrence } from "./recurrence.ts";
|
import type { Occurrence } from "./recurrence.ts";
|
||||||
import { EventType, GachaEvent, Precision, slugify } from "./schema.ts";
|
import { EventType, GachaEvent, Precision, slugify } from "./schema.ts";
|
||||||
|
|
||||||
@@ -271,3 +271,21 @@ export function asOccurrenceEvent(
|
|||||||
endPrecision: occurrence.endPrecision,
|
endPrecision: occurrence.endPrecision,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The stored record a row belongs to, whichever kind of id it carries.
|
||||||
|
*
|
||||||
|
* A row may be one occurrence of a rule, whose id carries a `#` suffix and is
|
||||||
|
* deliberately not a key in the store. Marks, ignores and ticks key off that
|
||||||
|
* suffixed id — each time round has its own completion — but there is only ever
|
||||||
|
* one record to edit, and it is the rule.
|
||||||
|
*
|
||||||
|
* Total, and safe for a feed id: `ruleIdOf` returns anything without a
|
||||||
|
* separator unchanged, and a feed id is simply not in this store.
|
||||||
|
*/
|
||||||
|
export function recordFor(
|
||||||
|
events: CustomEvents,
|
||||||
|
rowId: string,
|
||||||
|
): CustomEvent | undefined {
|
||||||
|
return events[ruleIdOf(rowId)];
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
mintCustomEventId,
|
mintCustomEventId,
|
||||||
mintCustomGameId,
|
mintCustomGameId,
|
||||||
precisionOf,
|
precisionOf,
|
||||||
|
recordFor,
|
||||||
RESERVED_ID_SEGMENTS,
|
RESERVED_ID_SEGMENTS,
|
||||||
type CustomGames,
|
type CustomGames,
|
||||||
} from "../src/shared/custom.ts";
|
} from "../src/shared/custom.ts";
|
||||||
@@ -607,3 +608,35 @@ describe("expanding rules into rows", () => {
|
|||||||
expect(rows).toEqual([]);
|
expect(rows).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("recordFor", () => {
|
||||||
|
const rule = ownEvent({
|
||||||
|
id: "myevent:k3f9qa2m01",
|
||||||
|
startsAt: new Date("2026-09-01T09:00:00").toISOString(),
|
||||||
|
startPrecision: "exact",
|
||||||
|
endsAt: new Date("2026-09-08T09:00:00").toISOString(),
|
||||||
|
endPrecision: "exact",
|
||||||
|
repeat: { unit: "weeks", interval: 2, until: null },
|
||||||
|
});
|
||||||
|
const store = { [rule.id]: rule };
|
||||||
|
|
||||||
|
test("an occurrence row finds the rule behind it", () => {
|
||||||
|
// Marks key off the occurrence — that is what gives each time round its own
|
||||||
|
// completion — but the record to edit is the rule. Without this the detail
|
||||||
|
// sheet looks up a key that does not exist and edit and delete vanish.
|
||||||
|
expect(recordFor(store, "myevent:k3f9qa2m01#2026-09-15")?.id).toBe("myevent:k3f9qa2m01");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a plain event finds itself", () => {
|
||||||
|
const plain = ownEvent({ id: "myevent:plain00001", repeat: null });
|
||||||
|
expect(recordFor({ [plain.id]: plain }, "myevent:plain00001")?.id).toBe("myevent:plain00001");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a feed event belongs to nobody here", () => {
|
||||||
|
expect(recordFor(store, "genshin:some-event:2026-09-01")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an occurrence of a rule the reader has since deleted finds nothing", () => {
|
||||||
|
expect(recordFor({}, "myevent:k3f9qa2m01#2026-09-15")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user