Dashboard

Headless hook

useInstaFixInbox — all the inbox logic, none of the UI. Build your own triage view.

Everything InstaFixInbox does — fetching, filters, optimistic mutations, undo, pagination — lives in one hook you can drive from your own components:

import { useInstaFixInbox } from "@instafix/dashboard";

const inbox = useInstaFixInbox({
  projects: "my-project",
  endpoint: "/api/instafix",
});

Options

The options are a union over the three source modes: supply exactly one of source, store or endpoint. Supplying none, supplying two, or pairing endpoint-only options (apiKey, headers) with store/source is a compile error — not a runtime throw and not a silently ignored option.

OptionTypeDefaultNotes
projectsstring or string[]requiredAn empty array throws at render. The first entry is selected initially
endpointstringHTTP mode
storeInstaFixStoreStore mode
sourceInboxSourceFully custom data source; see Data sources
apiKeystringEndpoint mode only. Authorization: Bearer on every request
headersobject or functionEndpoint mode only. An explicit Authorization beats apiKey. Read live — changing it doesn't rebuild the source
pageSizenumber50Clamped to 1–100
onStatusChange(feedback, previousStatus) => voidAfter the server confirms
onDelete(feedback) => voidAfter the server confirms
onError(error) => voidEvery failed load or mutation

Each mode is exported on its own — InboxEndpointOptions, InboxStoreOptions, InboxCustomSourceOptions, the shared InboxSharedOptions, and the UseInstaFixInboxOptions union — so a wrapper component can accept just the mode it supports. Plain-JavaScript callers get the old behavior: no source at all still throws at render.

What you get back

Dataitems (all loaded pages), total (null until the first page lands), counts (per-status totals, best-effort), loading, loadingMore, error, hasMore, loadMore(), refresh().

Filtersproject/setProject, status/setStatus (starts on "open"), type/setType, search/setSearch. Search updates state instantly and debounces the refetch by 250 ms; queries are trimmed and capped at 200 characters.

Focus & drawerfocusedId, focus(id), focusNext(), focusPrev(), openedId, opened (the open record — it stays available even if a filter removes its row), openFeedback(id), closeFeedback().

MutationschangeStatus(id, status), deleteFeedback(id), pendingUndo, undo().

view — skip the flag algebra

Deciding between a skeleton, an error state, an empty state and the list from loading / error / items.length is fiddly, and getting it subtly wrong is how a refetch ends up blanking the screen. view is that decision, already made — the exact value the shipped component renders from:

viewMeaning
"loading"The first page is loading and there is nothing displayable yet
"error"The load failed and there is nothing displayable
"empty"The load succeeded and no rows match the current filters
"ready"Rows are displayable
if (inbox.view === "loading") return <Skeleton />;
if (inbox.view === "error") return <ErrorState error={inbox.error} onRetry={inbox.refresh} />;
if (inbox.view === "empty") return <EmptyState />;
return <List items={inbox.items} />;

Rows win over every other signal: during a refetch the already-loaded rows stay on screen and view stays "ready", so the list never flashes back to a skeleton. Read loading when you want a granular spinner on top of visible rows.

Mutations are optimistic — and they re-throw

The UI updates immediately; if the server rejects, the hook rolls everything back (items, counts, focus, the open record) and then re-throws. Always attach a catch:

<button
  onClick={() => {
    inbox.changeStatus(item.id, "resolved").catch(() => {
      // state is already rolled back — surface a toast here
    });
  }}
>
  Resolve
</button>

Only the last status change is undoable (pendingUndo + undo()); delete is permanent, so gate it behind your own confirmation UI.

Pagination

loadMore() is manual — the bundled component renders a "Load more" button, not infinite scroll. Pages are deduplicated by id, and the next page number is derived from items.length, so optimistic removals never skip rows.

Edit on GitHub

On this page