refactor: introduce storage abstraction layer behind TodoStore - #63
Merged
Conversation
Refactors the existing Calendar-based data access behind a TodoStore
interface so future backends can be added without touching consumers.
Strict refactor — no user-visible behavior change, no new dependencies,
no new MSAL scopes.
Architecture:
- lib/store/types.ts: TodoStore, Book, ListItemsOptions, ID prefix helpers
- lib/store/multiStore.ts: MultiBackendStore (router; today routes only
to Calendar via 'cal:' prefix)
- lib/store/useStore.ts: React hook constructing the store
- lib/store/calendar/CalendarStore.ts: implements TodoStore against
Microsoft Graph Calendar API (was graphService.ts CRUD + todoDataService.ts)
- lib/store/calendar/utils.ts: ' by arrange' suffix logic, display name
- lib/store/calendar/bump.ts: date-bump helpers (calendar-specific)
- lib/store/calendar/types.ts: Graph response shapes
Other changes:
- graphService.ts trimmed to createGraphClient + getUserInfo
- todoDataService.ts and calendarUtils.ts removed (logic moved into store)
- Book IDs are now prefixed (cal:<id>) at the storage boundary
- Backward compat: unprefixed URLs and localStorage values are normalized
on read and the URL is replaced with the canonical prefixed form
- listItems now requires an explicit { range: 'all' | 'window' } option
(no implicit defaults — caller must specify)
- createBook takes { backend } so callers are future-proof
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors Arrange’s calendar-backed TODO data access behind a new TodoStore abstraction while keeping the current Microsoft Graph Calendar backend.
Changes:
- Introduces store interfaces, ID prefix helpers,
MultiBackendStore, andCalendarStore. - Moves calendar CRUD, TODO serialization/parsing, suffix utilities, and date-bump logic into
lib/store/calendar. - Updates pages/components to consume books/items through the store abstraction instead of direct Graph/todo services.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/arrange-v4/lib/todoDataService.ts |
Removed legacy TODO/calendar event data service. |
src/arrange-v4/lib/calendarUtils.ts |
Removed legacy Arrange-calendar helper module. |
src/arrange-v4/lib/graphService.ts |
Trimmed to Graph client creation and user info lookup. |
src/arrange-v4/lib/store/types.ts |
Adds store interfaces, TODO/book types, and book ID prefix helpers. |
src/arrange-v4/lib/store/useStore.ts |
Adds React hook for constructing the multi-backend store. |
src/arrange-v4/lib/store/multiStore.ts |
Adds backend router implementation for TodoStore. |
src/arrange-v4/lib/store/calendar/types.ts |
Adds narrow Graph calendar/event response shapes. |
src/arrange-v4/lib/store/calendar/utils.ts |
Adds calendar suffix filtering/display and Graph date conversion helpers. |
src/arrange-v4/lib/store/calendar/bump.ts |
Moves calendar-specific stale date bumping logic. |
src/arrange-v4/lib/store/calendar/CalendarStore.ts |
Implements TodoStore using Microsoft Graph Calendar APIs. |
src/arrange-v4/lib/hooks/useBookId.ts |
Refactors selected-book resolution to use store books and prefixed IDs. |
src/arrange-v4/components/AddTodoItem.tsx |
Updates TODO type imports to store types. |
src/arrange-v4/components/ViewTodoItem.tsx |
Updates TODO type imports to store types. |
src/arrange-v4/components/ManageTags.tsx |
Updates TODO type imports to store types. |
src/arrange-v4/components/ScrumCard.tsx |
Updates card props to use required item IDs from store types. |
src/arrange-v4/components/CalendarList.tsx |
Refactors list UI from calendars to abstract books. |
src/arrange-v4/app/page.tsx |
Refactors post-login routing and matrix availability checks through the store. |
src/arrange-v4/app/books/page.tsx |
Refactors book listing/create/delete to use the store. |
src/arrange-v4/app/matrix/page.tsx |
Refactors matrix item CRUD, listing, and sweep logic through the store. |
src/arrange-v4/app/scrum/page.tsx |
Refactors scrum board item CRUD/listing through the store. |
src/arrange-v4/app/cancelled/page.tsx |
Refactors cancelled item listing/deletion through the store. |
- app/page.tsx handleLogin: construct a one-shot MultiBackendStore using the access token from loginPopup so the listBooks() call doesn't race MSAL's React state update (useGraphToken would otherwise throw 'no account') - CalendarStore.createBook: use the case-insensitive ARRANGE_SUFFIX_REGEX (already used elsewhere) instead of an exact-match endsWith, so names like 'Project by Arrange' don't get a duplicate ' by arrange' appended Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uting error - app/page.tsx background availability check now uses acquireTokenSilent directly (no popup fallback). If the silent acquisition fails, we just skip showing the matrix buttons instead of opening an unexpected popup. - handleLogin: a transient listBooks failure no longer leaves the user stuck on the landing page. Post-login routing decision wrapped in its own try/catch with /books as the fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ilable - CalendarStore now deduplicates concurrent token acquisitions via an in-flight promise. Prevents the worst-case scenario of N concurrent popup attempts (each failing with interaction_in_progress) when a bulk operation runs after the silent token has expired. - parseBookId rejects unknown prefixes (values containing ':' that don't match a known backend) instead of silently treating them as legacy calendar IDs. Preserves the prefix-dispatch contract. - app/page.tsx background check now resets matrixAvailable on failure so stale data from a previous successful check or account switch doesn't linger. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CalendarStore.getToken: replace void .finally() chain with .then(clear, clear) so the chained promise can't surface as an unhandled rejection when token acquisition fails - useBookId: distinguish missing URL bookId from invalid URL bookId. Missing → fall back to saved book. Invalid (present but unknown prefix) → redirect to /books. Prevents silently loading a different book from a malformed link. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Owner
Author
PR Review Loop Summary
|
- CalendarStore.updateItem now honors caller-provided values for startDateTime, finishDateTime, originalEtsDateTime, originalEtaDateTime. The status-transition side-effects only fill in fields the caller did not explicitly set, preserving both the existing 'pass status, store derives timestamps' UX and the abstraction's documented contract. - bump.ts: comment clarified that bumping fires the moment an item becomes stale (ETS before today), not only after falling outside the ±30-day window. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use a 'cancelled' flag in the background availability check effect so a slow listBooks() response from a previous account can't clobber matrixAvailable state after the user switches accounts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Owner
Author
PR Review Loop Summary (updated)
|
This was referenced May 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes part of #62 — refactors the existing Calendar-based data access behind a
TodoStoreinterface so future backends can be added without touching the UI.This is a strict refactor. No user-visible behavior change, no new dependencies, no new MSAL scopes. The bottom-line recommendation from the decision document was to stay on Calendar and prepare the abstraction layer as a low-risk hedge — this PR is that hedge.
Why now (and why not Excel)
The original direction was to add Excel as a second backend. Validation against Microsoft's own docs confirmed Excel REST API doesn't support personal Microsoft accounts (
consumersauthority), which is what this app uses. The abstraction ships anyway — it's a small, low-risk refactor that opens the door for future backends (work/school Excel, client-side .xlsx upload, Google Sheets with a future auth abstraction, etc.) and cleans up the data layer regardless.Architecture
lib/todoDataService.tsandlib/calendarUtils.tsremoved — their logic moved into the store.Key design points (per rubber-duck feedback)
cal:<calendarId>. Routing dispatches on the prefix. Item IDs are bare backend IDs (they only exist within a Book).listItemsranges —{ range: 'all' | 'window' }. No implicit defaults. Matrix/Scrum use'window'; Cancelled uses'all'.createBooktakes abackend— required even though only one option today, so callers are future-proof.CalendarStore.sweepStaleItems()is not on theTodoStoreinterface. Matrix usesstore.calendar.sweepStaleItems(...)via the typed accessor./matrix?bookId=<raw>) and unprefixed localStorage values are normalized on read; URL is replaced with the canonical prefixed form.Diff size
The bulk of the diff is moving CRUD/serialization code into the new structure. The net line growth is from the new abstraction types and the multi-store router scaffolding.
Acceptance checklist
CalendarStore.graphServicefor CRUD or fromtodoDataService/calendarUtils(deleted).?bookId=<raw>is accepted and normalized.localStoragelastBookId is accepted and normalized.npm run buildpasses.npm run lintshows no new errors/warnings (pre-existing ones unchanged).Out of scope
AuthClientabstraction (storage only, per scope decision).The interface is designed to slot in additional backends later if the account model or Microsoft's Excel API support changes.