mirror of
https://github.com/ttusk/leif.git
synced 2026-07-22 08:32:51 +00:00
fix(errors): ValidationError for domain failures, runGuarded boundary
- AdvanceCycleUseCase and ReorderSubjectsUseCase now throw ValidationError instead of plain Error for domain rule violations. - Add DomHelpers.runGuarded error-boundary helper and use it in registerCommands (advance cycle, cycle snapshot, summary) to remove repeated try/catch + Notice boilerplate. Closes #6
This commit is contained in:
parent
c5eb9cf1ab
commit
d85711657a
5 changed files with 710 additions and 700 deletions
1357
sample-vault/.obsidian/plugins/leif/main.js
vendored
1357
sample-vault/.obsidian/plugins/leif/main.js
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@ import { ActiveCycleSnapshot } from "@/application/use-cases/GetActiveCycleSnaps
|
|||
import { CycleService } from "@/domain/services/CycleService";
|
||||
import { ItemProgressService } from "@/domain/services/ItemProgressService";
|
||||
import { ActiveContestGuard } from "@/application/guards/ActiveContestGuard";
|
||||
import { NotFoundError } from "@/domain/errors/DomainErrors";
|
||||
import { NotFoundError, ValidationError } from "@/domain/errors/DomainErrors";
|
||||
|
||||
/**
|
||||
* Use case for advancing the study cycle to the next subject.
|
||||
|
|
@ -38,7 +38,7 @@ export class AdvanceCycleUseCase {
|
|||
);
|
||||
|
||||
if (!nextSubject) {
|
||||
throw new Error(`Contest "${activeContestId}" has no active subjects.`);
|
||||
throw new ValidationError(`Contest "${activeContestId}" has no active subjects.`);
|
||||
}
|
||||
|
||||
const subjectItems = data.studyItems.filter(
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@ export class ReorderSubjectsUseCase {
|
|||
.filter((subject) => subject.contestId === input.contestId);
|
||||
|
||||
if (contestSubjects.length !== input.subjectIdsInOrder.length) {
|
||||
throw new Error("The provided subject order does not match the contest subject list.");
|
||||
throw new ValidationError("The provided subject order does not match the contest subject list.");
|
||||
}
|
||||
|
||||
const subjectIdSet = new Set(contestSubjects.map((subject) => subject.id));
|
||||
|
||||
for (const subjectId of input.subjectIdsInOrder) {
|
||||
if (!subjectIdSet.has(subjectId)) {
|
||||
throw new Error(`Subject "${subjectId}" does not belong to contest "${input.contestId}".`);
|
||||
throw new ValidationError(`Subject "${subjectId}" does not belong to contest "${input.contestId}".`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Plugin } from "obsidian";
|
|||
import { Notice } from "obsidian";
|
||||
|
||||
import type { PluginDataStore } from "@/application/ports/PluginDataStore";
|
||||
import { DomHelpers } from "@/ui/view/shared/DomHelpers";
|
||||
import { AdvanceCycleUseCase } from "@/application/use-cases/AdvanceCycleUseCase";
|
||||
import { CreateContestUseCase } from "@/application/use-cases/CreateContestUseCase";
|
||||
import { CreateStudyItemUseCase } from "@/application/use-cases/CreateStudyItemUseCase";
|
||||
|
|
@ -184,21 +185,18 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
|
|||
plugin.addCommand({
|
||||
id: "leif-advance-cycle",
|
||||
name: "Advance cycle",
|
||||
callback: async () => {
|
||||
try {
|
||||
callback: () =>
|
||||
DomHelpers.runGuarded(async () => {
|
||||
const state = await advanceCycle.execute();
|
||||
new Notice(`Current subject: ${state.currentSubjectId ?? "none"}`);
|
||||
} catch (error) {
|
||||
new Notice(error instanceof Error ? error.message : "Could not advance cycle.");
|
||||
}
|
||||
}
|
||||
}, "Could not advance cycle.")
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
id: "leif-show-cycle-snapshot",
|
||||
name: "Show cycle snapshot",
|
||||
callback: async () => {
|
||||
try {
|
||||
callback: () =>
|
||||
DomHelpers.runGuarded(async () => {
|
||||
const snapshot = await getActiveCycleSnapshot.execute();
|
||||
const data = await dataStore.load();
|
||||
const itemMap = new Map(data.studyItems.map((item) => [item.id, item.title]));
|
||||
|
|
@ -210,10 +208,7 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
|
|||
new Notice(
|
||||
`Current: ${currentLabel} | Next: ${nextLabel} | Current item: ${currentItemLabel} | Next item: ${nextItemLabel}`
|
||||
);
|
||||
} catch (error) {
|
||||
new Notice(error instanceof Error ? error.message : "Could not read cycle snapshot.");
|
||||
}
|
||||
}
|
||||
}, "Could not read cycle snapshot.")
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
|
|
@ -237,8 +232,8 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
|
|||
plugin.addCommand({
|
||||
id: "leif-show-summary",
|
||||
name: "Show active contest summary",
|
||||
callback: async () => {
|
||||
try {
|
||||
callback: () =>
|
||||
DomHelpers.runGuarded(async () => {
|
||||
const summary = await getActiveContestSummary.execute();
|
||||
|
||||
if (summary.subjectSummaries.length === 0) {
|
||||
|
|
@ -258,10 +253,7 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
|
|||
.join(" | ");
|
||||
|
||||
new Notice(message);
|
||||
} catch (error) {
|
||||
new Notice(error instanceof Error ? error.message : "Could not load active contest summary.");
|
||||
}
|
||||
}
|
||||
}, "Could not load active contest summary.")
|
||||
});
|
||||
|
||||
plugin.addCommand({
|
||||
|
|
|
|||
|
|
@ -493,6 +493,23 @@ export class DomHelpers {
|
|||
new Notice(error instanceof Error ? error.message : fallbackMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error-boundary helper: runs an async action and surfaces any failure
|
||||
* through notifyError so callers don't repeat try/catch + Notice boilerplate.
|
||||
*/
|
||||
static runGuarded(
|
||||
action: () => void | Promise<void>,
|
||||
fallbackMessage: string
|
||||
): Promise<void> {
|
||||
return (async () => {
|
||||
try {
|
||||
await action();
|
||||
} catch (error) {
|
||||
this.notifyError(error, fallbackMessage);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a modal overlay with a centered card.
|
||||
* Implements role=dialog, aria-modal, a focus trap and Escape-to-close.
|
||||
|
|
|
|||
Loading…
Reference in a new issue