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>
155 lines
5.4 KiB
TypeScript
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,
|
|
};
|
|
}
|