make create dashboard here actually create a dashboard here
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import { useState } from 'react';
|
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 { LayoutGrid, Plus, Pencil, Trash2, Search } from 'lucide-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useGlobal } from 'hooks/useGlobal';
|
import { useGlobal } from 'hooks/useGlobal';
|
||||||
import { useClient } from 'hooks/useClient';
|
import { useClient } from 'hooks/useClient';
|
||||||
import { generateSlug } from 'helpers/slug';
|
|
||||||
import { useUserState } from 'state/useUserState';
|
import { useUserState } from 'state/useUserState';
|
||||||
import { useDashboardState, persistDashboardState } from 'state/useDashboardState';
|
import { useDashboardState, persistDashboardState } from 'state/useDashboardState';
|
||||||
import type { DashboardDefinition } from '../../components/Workspace';
|
import type { DashboardDefinition } from '../../components/Workspace';
|
||||||
@@ -25,10 +24,10 @@ import {
|
|||||||
NEW_DASH_DESC_KEY,
|
NEW_DASH_DESC_KEY,
|
||||||
NEW_DASH_TEMPLATE_KEY,
|
NEW_DASH_TEMPLATE_KEY,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import { useNewDashboardDraft } from './useNewDashboardDraft';
|
||||||
|
|
||||||
export const DashboardListApp = () => {
|
export const DashboardListApp = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { value: dashboards, setValue: setDashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
|
const { value: dashboards, setValue: setDashboards } = useDashboardState<DashboardDefinition[]>('workspaces', []);
|
||||||
@@ -49,6 +48,9 @@ export const DashboardListApp = () => {
|
|||||||
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
const [, setDescription] = useGlobal<string>(NEW_DASH_DESC_KEY, '');
|
||||||
const [, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
const [, setTemplateIdx] = useGlobal<number>(NEW_DASH_TEMPLATE_KEY, 0);
|
||||||
const [, setFilePath] = useUserState<string>('files/currentPath', '/');
|
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 [search, setSearch] = useState('');
|
||||||
const [deleting, setDeleting] = useState<DashboardDefinition | null>(null);
|
const [deleting, setDeleting] = useState<DashboardDefinition | null>(null);
|
||||||
@@ -106,16 +108,7 @@ export const DashboardListApp = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => openNewDashboard()}
|
||||||
clearSelected();
|
|
||||||
setEditing(null);
|
|
||||||
setName(generateSlug());
|
|
||||||
setDescription('');
|
|
||||||
setTemplateIdx(0);
|
|
||||||
setFilePath('/');
|
|
||||||
setCreating(true);
|
|
||||||
if (!isDashboardsPage) navigate('/dashboards');
|
|
||||||
}}
|
|
||||||
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"
|
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" />
|
<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 { useUserState } from 'state/useUserState';
|
||||||
import { useAuth } from 'hooks/useAuth';
|
import { useAuth } from 'hooks/useAuth';
|
||||||
import { useFilesRefresh } from '../../../channels';
|
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
|
* 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) => {
|
export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPath = false) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const openNewDashboard = useNewDashboardDraft();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const homeRoot = 'home';
|
const homeRoot = 'home';
|
||||||
const [, setGlobalPath] = useUserState<string>('files/currentPath', '/');
|
const [, setGlobalPath] = useUserState<string>('files/currentPath', '/');
|
||||||
@@ -428,7 +430,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
|||||||
|
|
||||||
const handleCreateDashboard = (entry: DirEntry) => {
|
const handleCreateDashboard = (entry: DirEntry) => {
|
||||||
const folderPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`;
|
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) => {
|
const handleCopyPath = (entry: DirEntry) => {
|
||||||
@@ -452,9 +454,7 @@ export const useFileBrowserApp = (basePath: string, rootOverride?: string, urlPa
|
|||||||
|
|
||||||
const handleCreateDashboardHere = () => {
|
const handleCreateDashboardHere = () => {
|
||||||
const dirName = currentPath === '/' ? '' : currentPath.split('/').pop()!;
|
const dirName = currentPath === '/' ? '' : currentPath.split('/').pop()!;
|
||||||
const params = new URLSearchParams({ cwd: currentPath });
|
openNewDashboard({ name: dirName, path: currentPath });
|
||||||
if (dirName) params.set('name', dirName);
|
|
||||||
navigate(`/dashboards/new?${params}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReadAloud = async (entry: DirEntry) => {
|
const handleReadAloud = async (entry: DirEntry) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user