From 6c47cbeb74d5206ac21e367f40b228d86e30f71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 7 Aug 2026 11:51:45 +0000 Subject: [PATCH] make the email url the selection instead of a mirror of it Co-Authored-By: Claude Opus 5 --- docs/navigation-audit.md | 13 +++++---- docs/workspace-panel-todo.md | 15 +++++++++- .../Screens/Dashboard/Email/EmailList.tsx | 19 +++++++----- .../Screens/Dashboard/Email/EmailReader.tsx | 5 ++-- .../Screens/Dashboard/Email/EmailScreen.tsx | 29 ++++++------------- .../Screens/Dashboard/Email/shared.ts | 12 ++++++++ 6 files changed, 57 insertions(+), 36 deletions(-) create mode 100644 src/apps/officer-web/Screens/Dashboard/Email/shared.ts diff --git a/docs/navigation-audit.md b/docs/navigation-audit.md index 82d18ca0..8f777b79 100644 --- a/docs/navigation-audit.md +++ b/docs/navigation-audit.md @@ -35,7 +35,9 @@ That fix is the template for the HIGH items below. **Caveat:** the fix only did ## Route map (from `App.tsx`) -**Existing entity routes:** `/chat/:sessionId`, `/jobs/:id`, `/dashboards/:id`, `/projects/:id`, `/email/:emailId`. +**Existing entity routes:** `/chat/:sessionId`, `/jobs/:id`, `/dashboards/:id`, `/email/:emailId`. (`/projects/:id` +was in this list; the Projects feature was removed end to end on 2026-07-30, so the route is gone with it. H3 +and navigate-site 22 below still name it — they are the record of work that happened, not of code that exists.) **Flat screens:** `/` `/files` `/music` `/tasks` `/skills` `/processes` `/activity` `/system-monitor` `/task-logs` `/plans` `/terminal` `/desktop` `/browser` `/code-editor`. The five settings pages are route **pairs** now, not flat screens — `/settings/{profile,ai,system,integrations,user-management}` @@ -59,7 +61,7 @@ are good building blocks. The **Workspace/Panel framework** contains **zero** ro | H2 | `workspaces/…/apps/Dashboards/DashboardListApp.tsx:133` | a dashboard | `
` → `useGlobal(SELECTED_DASHBOARD_KEY)` on-page (**no URL change**), `navigate()` off-page | rows → ``; drop the global as selection source (derive from `useParams`). Header is already a `` — app is internally inconsistent. | | H3 | `workspaces/…/apps/Projects/ProjectListApp.tsx:161` | a project | `
` → `useGlobal(SELECTED_PROJECT)` on-page (**no URL change**), `navigate()` off-page | identical to H2 → ``; retire `SELECTED_PROJECT` as source of truth. | | H4 | `workspaces/…/apps/ChatHistory/ChatDetailPanel.tsx:136` | which chat to render | reads `usePanelChannel('chat:selected-session')`, **not** `useParams` | make the route the source of truth: read `sessionId` from `useParams`, fetch by id, retire `chat:selected-session` (or make it a derived cache). **Finishes the /chat fix.** | -| H5 | `Screens/Dashboard/Email/EmailList.tsx:336` | an email | `
- + ); })}
diff --git a/src/apps/officer-web/Screens/Dashboard/Email/EmailReader.tsx b/src/apps/officer-web/Screens/Dashboard/Email/EmailReader.tsx index d8708c01..f2b678e4 100644 --- a/src/apps/officer-web/Screens/Dashboard/Email/EmailReader.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Email/EmailReader.tsx @@ -4,9 +4,9 @@ import { Mail, Reply, Paperclip } from 'lucide-react'; import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'officerdev'; import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; import { useClient } from 'hooks/useClient'; -import { useGlobal } from 'hooks/useGlobal'; import type { EmailMessage, EmailThread } from 'types'; import { useComposer, replyDraft } from './Compose'; +import { useSelectedEmailId } from './shared'; type OpenAttachment = { filePath: string; @@ -123,7 +123,8 @@ export const EmailReader = () => { const client = useClient(); const queryClient = useQueryClient(); const [, openCompose] = useComposer(); - const [selectedId] = useGlobal('EMAIL_SELECTED', null); + // Reads the URL itself rather than being told by the list — the two panels never talk. + const selectedId = useSelectedEmailId(); const [openAttachment, setOpenAttachment] = useState(null); const [expanded, setExpanded] = useState>(new Set()); diff --git a/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx index 364f8fb1..274992a7 100644 --- a/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Email/EmailScreen.tsx @@ -1,14 +1,14 @@ -import { useMemo, useCallback, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router'; +import { useMemo, useCallback } from 'react'; +import { useNavigate } from 'react-router'; import type { LayoutNode, PanelComponents } from 'officerdev'; import { WorkspaceView, ChatPanelWrapper } from 'officerdev'; import { useDashboardState } from 'state/useDashboardState'; import { useIsMobile } from 'hooks/useIsMobile'; -import { useGlobal } from 'hooks/useGlobal'; import { defaultLayout } from './defaultLayout'; import { EmailList } from './EmailList'; import { EmailReader } from './EmailReader'; import { ComposeModal } from './Compose'; +import { useSelectedEmailId } from './shared'; const PROMPT_PREFIX = `You are an email assistant. The user has a local SQLite email database available via the "email_db" tool — use it for all email queries (search, count, stats, aggregations, deletions) unless the user explicitly asks you to use Gmail. Do not use the Gmail integration for questions about existing emails.`; @@ -19,23 +19,11 @@ const EmailChatPanel = () => { const isMobile = useIsMobile(); - const { emailId } = useParams<{ emailId?: string }>(); const navigate = useNavigate(); - const [selectedId, setSelectedId] = useGlobal('EMAIL_SELECTED', null); - - // Sync URL param → global state - useEffect(() => { - if (emailId && emailId !== selectedId) setSelectedId(emailId); - }, [emailId]); - - // Sync global state → URL - useEffect(() => { - if (selectedId && selectedId !== emailId) { - navigate(`/email/${selectedId}`, { replace: true }); - } else if (!selectedId && emailId) { - navigate('/email', { replace: true }); - } - }, [selectedId]); + // No `` guard: unlike a section route, the bare /email is a real state — the list with + // nothing open — exactly as /chat and /jobs are. An id that doesn't resolve is the reader's problem, + // not a reason to rewrite the address the user is looking at. + const selectedId = useSelectedEmailId(); const workspace = useDashboardState('screens/email', defaultLayout); const components: PanelComponents = useMemo( @@ -48,7 +36,8 @@ export const EmailScreen = () => { ); const mobilePanelId = isMobile && selectedId ? 'email-reader' : undefined; - const onMobileBack = useCallback(() => setSelectedId(null), [setSelectedId]); + // Back on mobile closes the email, which is now just navigating to the bare route. + const onMobileBack = useCallback(() => navigate('/email'), [navigate]); return (
diff --git a/src/apps/officer-web/Screens/Dashboard/Email/shared.ts b/src/apps/officer-web/Screens/Dashboard/Email/shared.ts new file mode 100644 index 00000000..8012e51c --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/Email/shared.ts @@ -0,0 +1,12 @@ +import { useParams } from 'react-router'; + +// Which email is open is `/email/:emailId`. The route pair already existed, but the URL was a mirror +// rather than the state: the selection lived in an `EMAIL_SELECTED` global and two effects copied it +// back and forth with the route. That round-trip is what kept a row a `