From d43f999b4977700f9de91f302e2bbf140eec483f Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 2 Nov 2026 15:32:15 -0600 Subject: [PATCH 1/7] feat(design): show status glyphs on the Actions board column headers --- __tests__/components/ui/actions/utils.test.ts | 15 -------------- src/components/ui/actions/kanban-column.tsx | 7 ++----- src/components/ui/actions/utils.ts | 20 ------------------- 3 files changed, 2 insertions(+), 40 deletions(-) diff --git a/__tests__/components/ui/actions/utils.test.ts b/__tests__/components/ui/actions/utils.test.ts index 65edff160..e942d8fc9 100644 --- a/__tests__/components/ui/actions/utils.test.ts +++ b/__tests__/components/ui/actions/utils.test.ts @@ -9,7 +9,6 @@ import { import type { AssignedActionWithContext } from "@/types/assigned-actions"; import { STATUS_COLUMN_ORDER, - statusColor, groupByStatus, applyTimeFilter, visibleStatuses, @@ -89,20 +88,6 @@ describe("actionStatusToString", () => { }); }); -// --------------------------------------------------------------------------- -// statusColor -// --------------------------------------------------------------------------- - -describe("statusColor", () => { - it("returns a Tailwind class for each status", () => { - expect(statusColor(ItemStatus.NotStarted)).toBe("bg-slate-400"); - expect(statusColor(ItemStatus.InProgress)).toBe("bg-blue-500"); - expect(statusColor(ItemStatus.OnHold)).toBe("bg-amber-400"); - expect(statusColor(ItemStatus.Completed)).toBe("bg-green-500"); - expect(statusColor(ItemStatus.WontDo)).toBe("bg-gray-400"); - }); -}); - // --------------------------------------------------------------------------- // groupByStatus // --------------------------------------------------------------------------- diff --git a/src/components/ui/actions/kanban-column.tsx b/src/components/ui/actions/kanban-column.tsx index 57486c595..896c3a5f9 100644 --- a/src/components/ui/actions/kanban-column.tsx +++ b/src/components/ui/actions/kanban-column.tsx @@ -3,7 +3,7 @@ import { memo } from "react"; import { useDroppable } from "@dnd-kit/core"; import { cn } from "@/components/lib/utils"; -import { statusColor } from "@/components/ui/actions/utils"; +import { StatusGlyph } from "@/components/ui/status/status-glyph"; import { actionStatusToString } from "@/types/general"; import { KanbanActionCard } from "@/components/ui/actions/kanban-action-card"; import type { KanbanCardCallbacks } from "@/components/ui/actions/kanban-action-card"; @@ -47,10 +47,7 @@ export const KanbanColumn = memo(function KanbanColumn({ > {/* Column header */}
- +

{actionStatusToString(status)}

{visibleCount} diff --git a/src/components/ui/actions/utils.ts b/src/components/ui/actions/utils.ts index 68fe10d3e..f650121b8 100644 --- a/src/components/ui/actions/utils.ts +++ b/src/components/ui/actions/utils.ts @@ -23,26 +23,6 @@ export const STATUS_COLUMN_ORDER: ItemStatus[] = [ ItemStatus.WontDo, ]; -/** Tailwind background color class for each status column indicator */ -export function statusColor(status: ItemStatus): string { - switch (status) { - case ItemStatus.NotStarted: - return "bg-slate-400"; - case ItemStatus.InProgress: - return "bg-blue-500"; - case ItemStatus.OnHold: - return "bg-amber-400"; - case ItemStatus.Completed: - return "bg-green-500"; - case ItemStatus.WontDo: - return "bg-gray-400"; - default: { - const _exhaustive: never = status; - throw new Error(`Unhandled status: ${_exhaustive}`); - } - } -} - /** Group items into status buckets, preserving order within each bucket */ export function groupByStatus( items: T[], From 3df99e465075f4f5312d325c16b0e494a9a5d9b7 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 2 Nov 2026 15:32:15 -0600 Subject: [PATCH 2/7] refactor(actions): share the action error message and goal progress refresh --- .../coaching-session-panel-topics.test.tsx | 1 + .../coaching-session-panel.test.tsx | 1 + .../goal-card-compact.test.tsx | 1 + .../topic-provenance-wiring.test.tsx | 1 + .../topic-rating-wiring.test.tsx | 1 + .../ui/dashboard/goals-overview-card.test.tsx | 1 + .../ui/actions/actions-page-container.tsx | 26 ++++----------- src/lib/api/goal-progress.ts | 16 ++++++++++ src/lib/hooks/use-panel-actions.ts | 32 +++++-------------- src/lib/utils/action-error-message.ts | 13 ++++++++ 10 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 src/lib/utils/action-error-message.ts diff --git a/__tests__/components/ui/coaching-sessions/coaching-session-panel-topics.test.tsx b/__tests__/components/ui/coaching-sessions/coaching-session-panel-topics.test.tsx index 62cef584b..a156901a7 100644 --- a/__tests__/components/ui/coaching-sessions/coaching-session-panel-topics.test.tsx +++ b/__tests__/components/ui/coaching-sessions/coaching-session-panel-topics.test.tsx @@ -80,6 +80,7 @@ vi.mock("@/lib/api/coaching-session-views", () => ({ })); vi.mock("@/lib/api/goal-progress", () => ({ + useRevalidateGoalProgress: () => vi.fn(), useGoalProgress: vi.fn(() => ({ progressMetrics: undefined, isLoading: false, diff --git a/__tests__/components/ui/coaching-sessions/coaching-session-panel.test.tsx b/__tests__/components/ui/coaching-sessions/coaching-session-panel.test.tsx index f49249810..b9a5c614f 100644 --- a/__tests__/components/ui/coaching-sessions/coaching-session-panel.test.tsx +++ b/__tests__/components/ui/coaching-sessions/coaching-session-panel.test.tsx @@ -80,6 +80,7 @@ vi.mock("@/lib/api/coaching-session-views", () => ({ })); vi.mock("@/lib/api/goal-progress", () => ({ + useRevalidateGoalProgress: () => vi.fn(), useGoalProgress: vi.fn(() => ({ progressMetrics: { actions_completed: 3, diff --git a/__tests__/components/ui/coaching-sessions/goal-card-compact.test.tsx b/__tests__/components/ui/coaching-sessions/goal-card-compact.test.tsx index 57a76330b..4f58cb3c5 100644 --- a/__tests__/components/ui/coaching-sessions/goal-card-compact.test.tsx +++ b/__tests__/components/ui/coaching-sessions/goal-card-compact.test.tsx @@ -9,6 +9,7 @@ import { None } from "@/types/option" // Mock the goal progress hook so tests don't fire real API calls vi.mock("@/lib/api/goal-progress", () => ({ + useRevalidateGoalProgress: () => vi.fn(), useGoalProgress: () => ({ progressMetrics: { actions_completed: 3, diff --git a/__tests__/components/ui/coaching-sessions/topic-provenance-wiring.test.tsx b/__tests__/components/ui/coaching-sessions/topic-provenance-wiring.test.tsx index 20a669b09..ddd09e46f 100644 --- a/__tests__/components/ui/coaching-sessions/topic-provenance-wiring.test.tsx +++ b/__tests__/components/ui/coaching-sessions/topic-provenance-wiring.test.tsx @@ -75,6 +75,7 @@ vi.mock("@/lib/api/coaching-session-views", () => ({ })); vi.mock("@/lib/api/goal-progress", () => ({ + useRevalidateGoalProgress: () => vi.fn(), useGoalProgress: vi.fn(() => ({ progressMetrics: undefined, isLoading: false, diff --git a/__tests__/components/ui/coaching-sessions/topic-rating-wiring.test.tsx b/__tests__/components/ui/coaching-sessions/topic-rating-wiring.test.tsx index be6f01acd..1ff816fe3 100644 --- a/__tests__/components/ui/coaching-sessions/topic-rating-wiring.test.tsx +++ b/__tests__/components/ui/coaching-sessions/topic-rating-wiring.test.tsx @@ -88,6 +88,7 @@ vi.mock("@/lib/api/coaching-session-views", () => ({ })); vi.mock("@/lib/api/goal-progress", () => ({ + useRevalidateGoalProgress: () => vi.fn(), useGoalProgress: vi.fn(() => ({ progressMetrics: undefined, isLoading: false, diff --git a/__tests__/components/ui/dashboard/goals-overview-card.test.tsx b/__tests__/components/ui/dashboard/goals-overview-card.test.tsx index b6b176867..062a135d6 100644 --- a/__tests__/components/ui/dashboard/goals-overview-card.test.tsx +++ b/__tests__/components/ui/dashboard/goals-overview-card.test.tsx @@ -22,6 +22,7 @@ function apiError(status: number): EntityApiError { // ── Hook mocks ───────────────────────────────────────────────────────── const mockUseGoalProgressList = vi.fn(); vi.mock("@/lib/api/goal-progress", () => ({ + useRevalidateGoalProgress: () => vi.fn(), useGoalProgressList: (...args: unknown[]) => mockUseGoalProgressList(...args), })); diff --git a/src/components/ui/actions/actions-page-container.tsx b/src/components/ui/actions/actions-page-container.tsx index 856a9f8a0..9885eb6cd 100644 --- a/src/components/ui/actions/actions-page-container.tsx +++ b/src/components/ui/actions/actions-page-container.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation"; import { useReplaceSearchParams } from "@/lib/hooks/use-replace-search-params"; import { DateTime } from "ts-luxon"; import { toast } from "@/lib/toast"; +import { actionErrorMessage } from "@/lib/utils/action-error-message"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; import { useCurrentOrganization } from "@/lib/hooks/use-current-organization"; import { useCoachingRelationshipList } from "@/lib/api/coaching-relationships"; @@ -22,8 +23,6 @@ import type { Id, ItemStatus } from "@/types/general"; import { ActionsPageHeader } from "@/components/ui/actions/actions-page-header"; import { KanbanBoard } from "@/components/ui/actions/kanban-board"; import { - EntityApiError, - PERMISSION_DENIED_MESSAGE, viewPermissionDeniedMessage, isForbiddenError, } from "@/lib/api/entity-api"; @@ -47,17 +46,6 @@ function parseEnum( return valid.has(value as T) ? (value as T) : fallback; } -/** Maps a failed action mutation to a user-facing message. `action` reads as - * the verb phrase in "Couldn't " (e.g. "update the status"). */ -function mutationErrorMessage(err: unknown, action: string): string { - if (err instanceof EntityApiError) { - if (err.isForbidden()) return PERMISSION_DENIED_MESSAGE; - if (err.isNetworkError()) - return `Couldn't ${action} · Check your connection and try again`; - } - return `Couldn't ${action}`; -} - interface ActionsPageContainerProps { locale: string; } @@ -214,7 +202,7 @@ export function ActionsPageContainer({ locale }: ActionsPageContainerProps) { // No explicit refresh() — the optimistic update already moved the card // visually, and SWR will revalidate on its own schedule. } catch (err) { - toast.error(mutationErrorMessage(err, "update the status")); + toast.error(actionErrorMessage("update the status", err)); throw err; // Re-throw so the board can roll back optimistic updates } }, @@ -230,7 +218,7 @@ export function ActionsPageContainer({ locale }: ActionsPageContainerProps) { await updateAction(id, { ...ctx.action, due_by: Some(newDueBy) }); refresh(); } catch (err) { - toast.error(mutationErrorMessage(err, "update the due date")); + toast.error(actionErrorMessage("update the due date", err)); } }, [actionsWithContext, updateAction, refresh] @@ -245,7 +233,7 @@ export function ActionsPageContainer({ locale }: ActionsPageContainerProps) { await updateAction(id, { ...ctx.action, assignee_ids: assigneeIds }); refresh(); } catch (err) { - toast.error(mutationErrorMessage(err, "update the assignees")); + toast.error(actionErrorMessage("update the assignees", err)); } }, [actionsWithContext, updateAction, refresh] @@ -266,7 +254,7 @@ export function ActionsPageContainer({ locale }: ActionsPageContainerProps) { await updateAction(id, updated); refresh(); } catch (err) { - toast.error(mutationErrorMessage(err, "update the action")); + toast.error(actionErrorMessage("update the action", err)); } }, [actionsWithContext, updateAction, refresh] @@ -282,7 +270,7 @@ export function ActionsPageContainer({ locale }: ActionsPageContainerProps) { await updateAction(id, updated); refresh(); } catch (err) { - toast.error(mutationErrorMessage(err, "update the goal link")); + toast.error(actionErrorMessage("update the goal link", err)); } }, [actionsWithContext, updateAction, refresh] @@ -294,7 +282,7 @@ export function ActionsPageContainer({ locale }: ActionsPageContainerProps) { await deleteAction(id); refresh(); } catch (err) { - toast.error(mutationErrorMessage(err, "delete the action")); + toast.error(actionErrorMessage("delete the action", err)); } }, [deleteAction, refresh] diff --git a/src/lib/api/goal-progress.ts b/src/lib/api/goal-progress.ts index 8f34cbe0e..0581e51d5 100644 --- a/src/lib/api/goal-progress.ts +++ b/src/lib/api/goal-progress.ts @@ -1,5 +1,7 @@ // Goal progress API — dedicated module mirroring backend's goal_progress module pattern. +import { useCallback } from "react"; +import { useSWRConfig } from "swr"; import { siteConfig } from "@/site.config"; import { Id, ItemStatus } from "@/types/general"; import { @@ -186,3 +188,17 @@ export const useGoalProgressList = ( refresh, }; }; + +/** Revalidates every cached goal's progress, e.g. after a linked action's status changes. */ +export const useRevalidateGoalProgress = () => { + const { mutate } = useSWRConfig(); + return useCallback( + () => + mutate( + (key: unknown) => typeof key === "string" && key.includes("/goals/") && key.endsWith("/progress"), + undefined, + { revalidate: true } + ), + [mutate] + ); +}; diff --git a/src/lib/hooks/use-panel-actions.ts b/src/lib/hooks/use-panel-actions.ts index 6b4d7e0bd..ab54e604c 100644 --- a/src/lib/hooks/use-panel-actions.ts +++ b/src/lib/hooks/use-panel-actions.ts @@ -1,18 +1,19 @@ "use client"; import { useMemo, useState, useCallback, useEffect } from "react"; -import { useSWRConfig } from "swr"; import { toast as sonnerToast } from "@/lib/toast"; import { actionPreview } from "@/lib/utils/action-preview"; +import { actionErrorMessage } from "@/lib/utils/action-error-message"; import { useUserActionsList } from "@/lib/api/user-actions"; import { useActionMutation } from "@/lib/api/actions"; +import { useRevalidateGoalProgress } from "@/lib/api/goal-progress"; import { useCoachingSessionList } from "@/lib/api/coaching-sessions"; import { UserActionsScope } from "@/types/assigned-actions"; import { sortActionArray, defaultAction } from "@/types/action"; import { Some, None } from "@/types/option"; import type { Action } from "@/types/action"; import type { Id } from "@/types/general"; -import { ItemStatus, EntityApiError } from "@/types/general"; +import { ItemStatus } from "@/types/general"; import { SortOrder } from "@/types/sorting"; import { DateTime } from "ts-luxon"; import { filterReviewActions } from "@/lib/utils/session"; @@ -36,15 +37,6 @@ function findActionById( ); } -/** Show an error toast, distinguishing network errors from other failures. */ -function showActionError(verb: string, err: unknown) { - if (err instanceof EntityApiError && err.isNetworkError()) { - sonnerToast.error(`Couldn't ${verb} · Check your connection and try again`); - } else { - sonnerToast.error(`Couldn't ${verb}`); - } -} - // ── Sub-hooks ───────────────────────────────────────────────────── /** Wide date range for fetching all sessions in this relationship */ @@ -197,7 +189,6 @@ function useActionCrud( refresh: () => void ) { const { create, update, delete: deleteAction } = useActionMutation(); - const { mutate: globalMutate } = useSWRConfig(); const updateField = useCallback( async (id: Id, fields: Partial, verb: string) => { @@ -207,20 +198,13 @@ function useActionCrud( await update(id, { ...action, ...fields }); refresh(); } catch (err) { - showActionError(verb, err); + sonnerToast.error(actionErrorMessage(verb, err)); } }, [sessionActions, allRelationshipActions, update, refresh] ); - /** Revalidate all goal progress SWR caches after a goal link or status change. */ - const revalidateGoalProgress = useCallback(() => { - globalMutate( - (key: unknown) => typeof key === "string" && key.includes("/goals/") && key.endsWith("/progress"), - undefined, - { revalidate: true } - ); - }, [globalMutate]); + const revalidateGoalProgress = useRevalidateGoalProgress(); const handleCreate = useCallback( async (body: string, assigneeIds?: Id[], goalId?: Id, dueBy?: DateTime) => { @@ -241,7 +225,7 @@ function useActionCrud( revalidateGoalProgress(); } } catch (err) { - showActionError("create the action", err); + sonnerToast.error(actionErrorMessage("create the action", err)); } }, [coachingSessionId, userId, create, refresh, revalidateGoalProgress] @@ -257,7 +241,7 @@ function useActionCrud( refresh(); revalidateGoalProgress(); } catch (err) { - showActionError("update the goal link", err); + sonnerToast.error(actionErrorMessage("update the goal link", err)); } }, [sessionActions, allRelationshipActions, update, refresh, revalidateGoalProgress] @@ -300,7 +284,7 @@ function useActionCrud( await deleteAction(id); refresh(); } catch (err) { - showActionError("delete the action", err); + sonnerToast.error(actionErrorMessage("delete the action", err)); return; } diff --git a/src/lib/utils/action-error-message.ts b/src/lib/utils/action-error-message.ts new file mode 100644 index 000000000..e54c8f435 --- /dev/null +++ b/src/lib/utils/action-error-message.ts @@ -0,0 +1,13 @@ +import { EntityApiError, PERMISSION_DENIED_MESSAGE } from "@/lib/api/entity-api"; + +/** + * The toast for a failed action mutation. `verb` reads as the phrase in + * "Couldn't " (e.g. "update the status"). + */ +export function actionErrorMessage(verb: string, err: unknown): string { + if (err instanceof EntityApiError) { + if (err.isForbidden()) return PERMISSION_DENIED_MESSAGE; + if (err.isNetworkError()) return `Couldn't ${verb} · Check your connection and try again`; + } + return `Couldn't ${verb}`; +} From 5bbe6604e8ab6acbc3c51daa2df675b1bf6b9f72 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 2 Nov 2026 15:32:15 -0600 Subject: [PATCH 3/7] feat(dashboard): set an action's status from the session detail rows --- .../coaching-sessions-hover-detail.test.tsx | 48 +++++++++++++++++-- .../coaching-sessions-hover-detail.tsx | 31 +++++++++++- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/__tests__/components/ui/dashboard/coaching-sessions-hover-detail.test.tsx b/__tests__/components/ui/dashboard/coaching-sessions-hover-detail.test.tsx index 2a311071e..07d003eab 100644 --- a/__tests__/components/ui/dashboard/coaching-sessions-hover-detail.test.tsx +++ b/__tests__/components/ui/dashboard/coaching-sessions-hover-detail.test.tsx @@ -1,11 +1,19 @@ -import { render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DateTime, Settings } from "ts-luxon"; import { SessionHoverDetail } from "@/components/ui/dashboard/coaching-sessions-hover-detail"; import { ItemStatus } from "@/types/general"; import { Some } from "@/types/option"; +import { toast } from "@/lib/toast"; import { createMockAction, createMockEnrichedSession } from "../../../test-utils"; +const update = vi.fn(); +const revalidateGoalProgress = vi.fn(); +vi.mock("@/lib/api/actions", () => ({ useActionMutation: () => ({ update }) })); +vi.mock("@/lib/api/goal-progress", () => ({ useRevalidateGoalProgress: () => revalidateGoalProgress })); +vi.mock("@/lib/toast", () => ({ toast: { error: vi.fn() } })); + const NOW = DateTime.fromISO("2026-08-25T12:00:00Z", { zone: "utc" }); function renderWith(actions: ReturnType[]) { @@ -23,6 +31,8 @@ function renderWith(actions: ReturnType[]) { describe("SessionHoverDetail action rows (compact layout)", () => { const originalNow = Settings.now; beforeEach(() => { + vi.clearAllMocks(); + update.mockResolvedValue(undefined); Settings.now = () => NOW.toMillis(); }); afterEach(() => { @@ -42,11 +52,39 @@ describe("SessionHoverDetail action rows (compact layout)", () => { const row = screen.getByRole("link", { name: /Send Ehab the notes/ }); expect(row).toHaveAttribute("href", "/coaching-sessions/sess-9?panel=actions"); - expect(screen.getByRole("img", { name: "In Progress" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Status: In Progress" })).toBeInTheDocument(); expect(screen.getByText("Due Aug 20")).toHaveClass("text-warning-text"); expect(screen.getByText("In Progress", { selector: 'span[data-status="InProgress"]' })).toBeInTheDocument(); - // Display-only here: no status buttons on the dashboard. - expect(screen.queryByRole("button", { name: /^Status:/ })).not.toBeInTheDocument(); + }); + + it("saves a status chosen from the row", async () => { + const action = createMockAction({ id: "a1", body: "Send Ehab the notes", status: ItemStatus.InProgress }); + renderWith([action]); + + await userEvent.click(screen.getByRole("button", { name: "Status: In Progress" })); + await userEvent.click(screen.getByRole("menuitemradio", { name: /Completed/ })); + + expect(update).toHaveBeenCalledWith("a1", { ...action, status: ItemStatus.Completed }); + expect(revalidateGoalProgress).not.toHaveBeenCalled(); + }); + + it("refreshes goal progress when the action is linked to a goal", async () => { + renderWith([createMockAction({ id: "a1", status: ItemStatus.InProgress, goal_id: Some("goal-1") })]); + + await userEvent.click(screen.getByRole("button", { name: "Status: In Progress" })); + await userEvent.click(screen.getByRole("menuitemradio", { name: /Completed/ })); + + await waitFor(() => expect(revalidateGoalProgress).toHaveBeenCalled()); + }); + + it("says so when the status can't be saved", async () => { + update.mockRejectedValue(new Error("boom")); + renderWith([createMockAction({ id: "a1", status: ItemStatus.InProgress })]); + + await userEvent.click(screen.getByRole("button", { name: "Status: In Progress" })); + await userEvent.click(screen.getByRole("menuitemradio", { name: /Completed/ })); + + await waitFor(() => expect(toast.error).toHaveBeenCalledWith("Couldn't update the status")); }); it("mutes a completed action without striking it through", () => { diff --git a/src/components/ui/dashboard/coaching-sessions-hover-detail.tsx b/src/components/ui/dashboard/coaching-sessions-hover-detail.tsx index 89e79c575..ef00dbaec 100644 --- a/src/components/ui/dashboard/coaching-sessions-hover-detail.tsx +++ b/src/components/ui/dashboard/coaching-sessions-hover-detail.tsx @@ -5,8 +5,13 @@ import { MessageSquare } from "lucide-react"; import { SessionGoalList } from "@/components/ui/session-goal-list"; import { ActionStatusRow } from "@/components/ui/status/action-status-row"; import { PanelSection } from "@/components/ui/coaching-sessions/coaching-session-panel-selector"; +import { useActionMutation } from "@/lib/api/actions"; +import { useRevalidateGoalProgress } from "@/lib/api/goal-progress"; +import { toast } from "@/lib/toast"; +import { actionErrorMessage } from "@/lib/utils/action-error-message"; import { formatDateWithTime } from "@/lib/utils/date"; import type { Action } from "@/types/action"; +import type { ItemStatus } from "@/types/general"; import { isPastSession, type EnrichedCoachingSession, @@ -39,6 +44,7 @@ export function SessionHoverDetail({ reviewActions, locale, }: SessionHoverDetailProps) { + const setStatus = useSetActionStatus(); const goals = session.goals ?? []; const scheduledDateTime = DateTime.fromISO(session.date, { zone: "utc", @@ -91,7 +97,7 @@ export function SessionHoverDetail({ // with `?panel=actions` so the Actions panel opens automatically.
{reviewActions.map((action) => ( - + ))}
)} @@ -100,7 +106,27 @@ export function SessionHoverDetail({ ); } -function ActionDueRow({ action, locale }: { action: Action; locale: string }) { +/** Saves a new status; the action lists and any linked goal's progress refresh after. */ +function useSetActionStatus() { + const { update } = useActionMutation(); + const revalidateGoalProgress = useRevalidateGoalProgress(); + return async (action: Action, status: ItemStatus) => { + try { + await update(action.id, { ...action, status }); + if (action.goal_id.some) revalidateGoalProgress(); + } catch (err) { + toast.error(actionErrorMessage("update the status", err)); + } + }; +} + +interface ActionDueRowProps { + action: Action; + locale: string; + onStatusChange: (action: Action, status: ItemStatus) => void; +} + +function ActionDueRow({ action, locale, onStatusChange }: ActionDueRowProps) { return ( onStatusChange(action, status)} className="-mx-3" /> ); From 6661451279d923255e2d435d8bb4229932202731 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 2 Nov 2026 15:32:15 -0600 Subject: [PATCH 4/7] fix(design): use 16px page padding on phones --- src/app/admin/layout.tsx | 2 +- src/app/dashboard/layout.tsx | 2 +- src/app/members/[id]/profile/layout.tsx | 2 +- src/app/organizations/[id]/members/layout.tsx | 2 +- src/app/settings/layout.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 467944bd4..92821be12 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -18,7 +18,7 @@ export default function AdminLayout({ return (
-
+
{children}
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 0b072cf37..ea75ae06d 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -17,7 +17,7 @@ export default function DashboardLayout({ return (
-
{children}
+
{children}
); } diff --git a/src/app/members/[id]/profile/layout.tsx b/src/app/members/[id]/profile/layout.tsx index 175ee9e20..80cbc06ad 100644 --- a/src/app/members/[id]/profile/layout.tsx +++ b/src/app/members/[id]/profile/layout.tsx @@ -17,7 +17,7 @@ export default function MembersLayout({ return (
-
{children}
+
{children}
); } diff --git a/src/app/organizations/[id]/members/layout.tsx b/src/app/organizations/[id]/members/layout.tsx index 04aad707f..3d1c7eda0 100644 --- a/src/app/organizations/[id]/members/layout.tsx +++ b/src/app/organizations/[id]/members/layout.tsx @@ -17,7 +17,7 @@ export default function MembersLayout({ return (
-
{children}
+
{children}
); } diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx index 0a340aca3..68a0c7d08 100644 --- a/src/app/settings/layout.tsx +++ b/src/app/settings/layout.tsx @@ -19,7 +19,7 @@ export default function SettingsLayout({ return (
-
+
{/* SettingsNav renders both mobile (horizontal tabs above) From 7742221b5efe40dbefdcae19865720e6f63917e7 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 2 Nov 2026 15:32:16 -0600 Subject: [PATCH 5/7] refactor(design): use the tertiary variant for text buttons --- .../ui/coaching-sessions/action-card-compact.tsx | 4 ++-- .../ui/coaching-sessions/agreement-card-compact.tsx | 4 ++-- .../ui/coaching-sessions/coaching-session-panel.tsx | 4 ++-- .../ui/coaching-sessions/dismissed-suggestions.tsx | 2 +- src/components/ui/coaching-sessions/goal-browse-view.tsx | 4 ++-- src/components/ui/coaching-sessions/goal-create-form.tsx | 4 ++-- src/components/ui/coaching-sessions/goal-edit-form.tsx | 4 ++-- .../ui/coaching-sessions/suggested-action-card.tsx | 8 ++++---- src/components/ui/dashboard/goals-overview-card.tsx | 4 ++-- src/components/ui/dashboard/series-detail-dialog.tsx | 4 ++-- .../ui/dashboard/session-buckets/bucket-list.tsx | 4 ++-- src/components/ui/members/add-member-dialog.tsx | 2 +- src/components/ui/members/member-card.tsx | 2 +- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/components/ui/coaching-sessions/action-card-compact.tsx b/src/components/ui/coaching-sessions/action-card-compact.tsx index 179cd34ce..cf17d17bc 100644 --- a/src/components/ui/coaching-sessions/action-card-compact.tsx +++ b/src/components/ui/coaching-sessions/action-card-compact.tsx @@ -650,9 +650,9 @@ export function ActionEditForm({ {saveLabel}