Files
fusion/packages/dashboard/app/hooks/useProjectActions.ts
gsxdsm 3453d164ea FN-7416: preserve dashboard project selection in URL
Keep dashboard project selection in the URL so refreshing or sharing the page retains the active project.

- Hydrate current project state from the existing ?project= query parameter before falling back to cached or global defaults.
- Update project selection, setup completion, overview, and unregister flows to preserve or clear the project URL parameter without dropping other URL state.
- Cover URL-driven project persistence and query/hash preservation with hook tests.
- Document the refresh-safe project URL behavior and add a patch changeset.

Files changed:
 .changeset/fn-7416-project-url-persistence.md      |  7 +++
 docs/dashboard-guide.md                            |  2 +
 packages/dashboard/app/App.tsx                     |  2 +-
 .../app/hooks/__tests__/useCurrentProject.test.ts  | 64 ++++++++++++++++++++++
 .../app/hooks/__tests__/useProjectActions.test.ts  | 40 +++++++++++++-
 packages/dashboard/app/hooks/useCurrentProject.ts  | 54 ++++++++++++++++--
 packages/dashboard/app/hooks/useProjectActions.ts  |  5 ++
 packages/dashboard/app/utils/projectUrlState.ts    | 24 ++++++++
 8 files changed, 190 insertions(+), 8 deletions(-)

Fusion-Task-Id: FN-7416

Fusion-Task-Lineage: 2d640ae6-629f-4632-9014-986320acfef5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:31 -07:00

155 lines
5.4 KiB
TypeScript

import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { pauseProject, resumeProject, unregisterProject } from "../api";
import type { ProjectInfo } from "../api";
import { replaceProjectIdInUrl } from "../utils/projectUrlState";
import type { ViewMode } from "./useViewState";
import type { ToastType } from "./useToast";
interface UseProjectActionsOptions {
setCurrentProject: (project: ProjectInfo) => void;
clearCurrentProject: () => void;
setViewMode: (mode: ViewMode) => void;
currentProject: ProjectInfo | null;
refreshProjects: () => Promise<void>;
toggleFavoriteProvider: (provider: string) => Promise<void>;
toggleFavoriteModel: (modelId: string) => Promise<void>;
addToast: (message: string, type: ToastType) => void;
openSettings: () => void;
openSetupWizard: () => void;
closeSetupWizard: () => void;
closeModelOnboarding: () => void;
}
export interface UseProjectActionsResult {
handleSelectProject: (project: ProjectInfo) => void;
handleViewAllProjects: () => void;
handleOpenSettings: () => void;
handleAddProject: () => void;
handleSetupComplete: (project: ProjectInfo) => void;
handleModelOnboardingComplete: () => void;
handlePauseProject: (project: ProjectInfo) => Promise<void>;
handleResumeProject: (project: ProjectInfo) => Promise<void>;
handleRemoveProject: (project: ProjectInfo) => Promise<void>;
handleToggleFavorite: (provider: string) => Promise<void>;
handleToggleModelFavorite: (modelId: string) => Promise<void>;
}
export function useProjectActions(options: UseProjectActionsOptions): UseProjectActionsResult {
const { t } = useTranslation("app");
const {
setCurrentProject,
clearCurrentProject,
setViewMode,
currentProject,
refreshProjects,
toggleFavoriteProvider,
toggleFavoriteModel,
addToast,
openSettings,
openSetupWizard,
closeSetupWizard,
closeModelOnboarding,
} = options;
const handleSelectProject = useCallback((project: ProjectInfo) => {
replaceProjectIdInUrl(project.id);
setCurrentProject(project);
setViewMode("project");
}, [setCurrentProject, setViewMode]);
const handleViewAllProjects = useCallback(() => {
replaceProjectIdInUrl(null);
clearCurrentProject();
setViewMode("overview");
}, [clearCurrentProject, setViewMode]);
const handleOpenSettings = useCallback(() => {
openSettings();
}, [openSettings]);
const handleAddProject = useCallback(() => {
openSetupWizard();
}, [openSetupWizard]);
const handleSetupComplete = useCallback((project: ProjectInfo) => {
closeSetupWizard();
replaceProjectIdInUrl(project.id);
setCurrentProject(project);
setViewMode("project");
addToast(t("projects.setup.success", "Project {{name}} registered successfully", { name: project.name }), "success");
void refreshProjects();
}, [closeSetupWizard, setCurrentProject, setViewMode, addToast, refreshProjects, t]);
const handleModelOnboardingComplete = useCallback(() => {
closeModelOnboarding();
}, [closeModelOnboarding]);
const handlePauseProject = useCallback(async (project: ProjectInfo) => {
try {
await pauseProject(project.id);
addToast(t("projects.actions.pauseSuccess", "Project {{name}} paused", { name: project.name }), "success");
await refreshProjects();
} catch {
addToast(t("projects.actions.pauseError", "Failed to pause project {{name}}", { name: project.name }), "error");
}
}, [addToast, refreshProjects, t]);
const handleResumeProject = useCallback(async (project: ProjectInfo) => {
try {
await resumeProject(project.id);
addToast(t("projects.actions.resumeSuccess", "Project {{name}} resumed", { name: project.name }), "success");
await refreshProjects();
} catch {
addToast(t("projects.actions.resumeError", "Failed to resume project {{name}}", { name: project.name }), "error");
}
}, [addToast, refreshProjects, t]);
const handleRemoveProject = useCallback(async (project: ProjectInfo) => {
try {
await unregisterProject(project.id);
addToast(t("projects.actions.removeSuccess", "Project {{name}} removed", { name: project.name }), "success");
if (currentProject?.id === project.id) {
replaceProjectIdInUrl(null);
clearCurrentProject();
setViewMode("overview");
}
await refreshProjects();
} catch {
addToast(t("projects.actions.removeError", "Failed to remove project {{name}}", { name: project.name }), "error");
}
}, [currentProject, clearCurrentProject, setViewMode, addToast, refreshProjects, t]);
const handleToggleFavorite = useCallback(async (provider: string) => {
try {
await toggleFavoriteProvider(provider);
} catch {
addToast(t("projects.actions.favoritesError", "Failed to update favorites"), "error");
}
}, [toggleFavoriteProvider, addToast, t]);
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
try {
await toggleFavoriteModel(modelId);
} catch {
addToast(t("projects.actions.modelFavoritesError", "Failed to update model favorites"), "error");
}
}, [toggleFavoriteModel, addToast, t]);
return {
handleSelectProject,
handleViewAllProjects,
handleOpenSettings,
handleAddProject,
handleSetupComplete,
handleModelOnboardingComplete,
handlePauseProject,
handleResumeProject,
handleRemoveProject,
handleToggleFavorite,
handleToggleModelFavorite,
};
}