make create dashboard here actually create a dashboard here

This commit is contained in:
2026-08-07 13:58:28 +00:00
parent f4fdc000b2
commit 19ba106265
3 changed files with 67 additions and 17 deletions
@@ -1,10 +1,9 @@
import { useState } from 'react';
import { Link, useLocation, useNavigate, useSearchParams } from 'react-router';
import { Link, useLocation, useSearchParams } from 'react-router';
import { LayoutGrid, Plus, Pencil, Trash2, Search } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { useGlobal } from 'hooks/useGlobal';
import { useClient } from 'hooks/useClient';
import { generateSlug } from 'helpers/slug';
import { useUserState } from 'state/useUserState';
import { useDashboardState, persistDashboardState } from 'state/useDashboardState';
import type { DashboardDefinition } from '../../components/Workspace';
@@ -25,10 +24,10 @@ import {
NEW_DASH_DESC_KEY,
NEW_DASH_TEMPLATE_KEY,
} from './constants';
import { useNewDashboardDraft } from './useNewDashboardDraft';
export const DashboardListApp = () => {
const location = useLocation();
const navigate = useNavigate();
const client = useClient();
const queryClient = useQueryClient();
const { value: dashboards, setValue: setDashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
@@ -49,6 +48,9 @@ export const DashboardListApp = () => {
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
const [, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
const [, setFilePath] = useUserState<string>('files/currentPath', '/');
// Shared with the file browser's "Create Dashboard here", which needs the same five writes seeded from
// a folder rather than from nothing.
const openNewDashboard = useNewDashboardDraft();
const [search, setSearch] = useState('');
const [deleting, setDeleting] = useState<DashboardDefinition | null>(null);
@@ -106,16 +108,7 @@ export const DashboardListApp = () => {
</Link>
<button
type="button"
onClick={() => {
clearSelected();
setEditing(null);
setName(generateSlug());
setDescription('');
setTemplateIdx(0);
setFilePath('/');
setCreating(true);
if (!isDashboardsPage) navigate('/dashboards');
}}
onClick={() => openNewDashboard()}
className="flex items-center justify-center gap-1 py-2 px-3 rounded-lg text-sm font-medium bg-duck-teal hover:bg-duck-teal/90 text-white transition-all cursor-pointer"
>
<Plus className="h-4 w-4 shrink-0" />
@@ -0,0 +1,57 @@
import { useLocation, useNavigate } from 'react-router';
import { useGlobal } from 'hooks/useGlobal';
import { useUserState } from 'state/useUserState';
import { generateSlug } from 'helpers/slug';
import {
CREATING_DASHBOARD_KEY,
EDITING_DASHBOARD_KEY,
NEW_DASH_NAME_KEY,
NEW_DASH_DESC_KEY,
NEW_DASH_TEMPLATE_KEY,
} from './constants';
type NewDashboardSeed = {
/** Pre-fills the name field. Falls back to a generated slug, which is what the plain button does. */
name?: string;
/** Home-relative folder the dashboard should be rooted at, leading slash, e.g. `/Pictures/Photos`. */
path?: string;
};
/**
* Opens the create-dashboard form, optionally seeded. Returns the opener, not state.
*
* The seed is spread across four `useGlobal` form-draft keys plus `files/currentPath` — `NewDashboardForm`
* derives its `cwd` from that last one rather than holding a folder field of its own — so "start creating a
* dashboard" is five writes in a specific order, not one. It lived inline in the New Dashboard button, and
* the file browser's "Create Dashboard here" tried to express the same intent as a URL instead
* (`/dashboards/new?name=…&cwd=…`). That URL never had a route: `new` matched `/dashboards/:id`, found no
* dashboard by that id, and redirected to the bare list, dropping the params — so the menu item had been
* doing nothing since the workspaces→dashboards rename in `927267e`. One opener, two call sites, no URL
* contract to keep in sync.
*
* Seeding a draft *and then* navigating is why the callers stay buttons rather than becoming links: a link
* cannot express the "and then".
*/
export const useNewDashboardDraft = () => {
const location = useLocation();
const navigate = useNavigate();
const [, setCreating] = useGlobal<boolean>(CREATING_DASHBOARD_KEY, false);
const [, setEditing] = useGlobal<string | null>(EDITING_DASHBOARD_KEY, null);
const [, setName] = useGlobal<string>(NEW_DASH_NAME_KEY, '');
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
const [, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
const [, setFilePath] = useUserState<string>('files/currentPath', '/');
return ({ name, path }: NewDashboardSeed = {}) => {
setEditing(null);
setName(name?.trim() || generateSlug());
setDescription('');
setTemplateIdx(0);
setFilePath(path || '/');
setCreating(true);
// `/dashboards` with no query — which is also how `?selected=` gets cleared, since an open form and a
// selected dashboard are different states. Replace when already there so opening the form is not a
// history entry you have to press Back through twice.
navigate('/dashboards', { replace: location.pathname === '/dashboards' });
};
};
@@ -7,6 +7,7 @@ import { useAgents, type AgentSummary } from '../useAgents';
import { useUserState } from 'state/useUserState';
import { useAuth } from 'hooks/useAuth';
import { useFilesRefresh } from '../../../channels';
import { useNewDashboardDraft } from '../../Dashboards/useNewDashboardDraft';
/**
* Where the browser is looking, when it is the one browser that owns the address bar. `urlPath` is opt-in
@@ -26,6 +27,7 @@ const withPath = (prev: URLSearchParams, path: string, basePath: string) => {
export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPath = false) => {
const { user } = useAuth();
const navigate = useNavigate();
const openNewDashboard = useNewDashboardDraft();
const [searchParams, setSearchParams] = useSearchParams();
const homeRoot = 'home';
const [, setGlobalPath] = useUserState<string>('files/currentPath', '/');
@@ -428,7 +430,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
const handleCreateDashboard = (entry: DirEntry) => {
const folderPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
navigate(`/dashboards/new?name=${encodeURIComponent(entry.name)}&cwd=${encodeURIComponent(folderPath)}`);
openNewDashboard({ name: entry.name, path: folderPath });
};
const handleCopyPath = (entry: DirEntry) => {
@@ -452,9 +454,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
const handleCreateDashboardHere = () => {
const dirName = currentPath === '/' ? '' : currentPath.split('/').pop()!;
const params = new URLSearchParams({ cwd: currentPath });
if (dirName) params.set('name', dirName);
navigate(`/dashboards/new?${params}`);
openNewDashboard({ name: dirName, path: currentPath });
};
const handleReadAloud = async (entry: DirEntry) => {