projects: drive list selection from a ?selected= url param, not a global

mirror of the dashboards change: the /projects list row was a <div onClick> setting
SELECTED_PROJECT (a global) with no url change on-page. selection now lives in the url
as ?selected=<id>, read by the list, the screen (mobile panel), and the preview. rows
are real <Link>s (publish/edit/delete kept as sibling buttons); edit-save and delete
update/clear the param; NewProjectRedirect drops its now-redundant setSelected.

NOT runtime-tested yet (server not restarted) — verify /projects preview,
create/edit/delete/publish, and mobile panel flows on next restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 05:35:24 +00:00
co-authored by Claude Opus 4.8
parent 1b6b980af0
commit 63f45de869
3 changed files with 77 additions and 51 deletions
@@ -6,7 +6,6 @@ import { useDashboardState } from 'state/useDashboardState';
import type { LayoutNode, ProjectType } from 'officerdev';
import {
WorkspaceView,
SELECTED_PROJECT,
CREATING_PROJECT,
EDITING_PROJECT,
NEW_PROJ_NAME,
@@ -22,7 +21,9 @@ import { defaultLayout } from './defaultLayout';
export const ProjectListScreen = () => {
const workspace = useDashboardState<LayoutNode>('screens/projects', defaultLayout);
const isMobile = useIsMobile();
const [selected, setSelected] = useGlobal<string | null>(SELECTED_PROJECT, null);
// Selection lives in the URL (?selected=<id>); on mobile it (or an active create) drives the panel shown.
const [searchParams, setSearchParams] = useSearchParams();
const selected = searchParams.get('selected');
const [creating] = useGlobal<boolean>(CREATING_PROJECT, false);
const mobilePanelId = isMobile && (selected || creating) ? 'proj-home-right' : undefined;
@@ -33,7 +34,14 @@ export const ProjectListScreen = () => {
locked
mobilePanelId={mobilePanelId}
onMobilePanelChange={(id) => {
if (!id) setSelected(null);
if (!id)
setSearchParams(
(p) => {
p.delete('selected');
return p;
},
{ replace: true },
);
}}
/>
</div>
@@ -48,13 +56,12 @@ export const NewProjectRedirect = () => {
const [, setTemplateIdx] = useGlobal<number>(NEW_PROJ_TEMPLATE, 0);
const [, setCreating] = useGlobal<boolean>(CREATING_PROJECT, false);
const [, setEditing] = useGlobal<string | null>(EDITING_PROJECT, null);
const [, setSelected] = useGlobal<string | null>(SELECTED_PROJECT, null);
const [, setProjectType] = useGlobal<ProjectType>(NEW_PROJ_TYPE, 'app');
const [, setHasBackend] = useGlobal<boolean>(NEW_PROJ_HAS_BACKEND, false);
const [, setHasAuth] = useGlobal<boolean>(NEW_PROJ_HAS_AUTH, false);
useEffect(() => {
setSelected(null);
// Redirect lands on a clean /projects (no ?selected), so selection is naturally cleared.
setEditing(null);
setName(params.get('name') || generateSlug());
setDescription('');