Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/api/src/app/api/electric/[...path]/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
taskStatuses,
tasks,
v2Clients,
v2HostProjects,
v2Hosts,
v2Projects,
v2UsersHosts,
Expand All @@ -34,6 +35,7 @@ export type AllowedTable =
| "v2_projects"
| "v2_users_hosts"
| "v2_workspaces"
| "v2_host_projects"
| "auth.members"
| "auth.organizations"
| "auth.users"
Expand Down Expand Up @@ -96,6 +98,13 @@ export async function buildWhereClause(
case "v2_workspaces":
return build(v2Workspaces, v2Workspaces.organizationId, organizationId);

case "v2_host_projects":
return build(
v2HostProjects,
v2HostProjects.organizationId,
organizationId,
);

case "auth.members":
return build(members, members.organizationId, organizationId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { toast } from "@superset/ui/sonner";
import { useEffect } from "react";
import {
useAddRepositoryModalActive,
useCloseAddRepositoryModal,
useFolderImportTrigger,
} from "renderer/stores/add-repository-modal";
import { FolderFirstImportModal } from "../../v2-workspaces/components/FolderFirstImportModal";
import { NewProjectModal } from "../../v2-workspaces/components/NewProjectModal";
import { PinAndSetupModal } from "../../v2-workspaces/components/PinAndSetupModal";
import { useFolderFirstImport } from "../../v2-workspaces/hooks/useFolderFirstImport";

/**
* Layout-level host for the three add-repository flows (New project, Import
* existing folder, Pin & set up). Any component in the dashboard can open
* one via the `useAddRepositoryModalStore` actions — sidebar dropdown,
* workspaces-tab Available rows, future empty-state CTAs, etc.
*
* Why centralize: modal state lives once per app, not once per trigger.
* Also keeps the folder-first picker's internal state machine in one place
* so nothing races if two triggers happen quickly.
*/
export function AddRepositoryModals() {
const active = useAddRepositoryModalActive();
const close = useCloseAddRepositoryModal();
const folderImportTrigger = useFolderImportTrigger();

const folderImport = useFolderFirstImport({
onSuccess: () => {
toast.success("Project ready — open it from the sidebar.");
},
onError: (message) => {
toast.error(`Import failed: ${message}`);
},
});

// Run the folder-first picker when the store's trigger counter bumps.
// Using a counter (vs a boolean) lets successive clicks re-invoke the
// flow after the previous one resolves.
useEffect(() => {
if (folderImportTrigger === 0) return;
void folderImport.start();
// We intentionally depend only on the counter — folderImport.start's
// identity changes every render (new hook instance per render) and
// we don't want to restart the flow on those changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [folderImportTrigger, folderImport.start]);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The effect dependency list includes folderImport.start, which is not stable here and can repeatedly re-trigger start() after a single trigger bump. Depend only on folderImportTrigger for this counter-based pulse effect.

(Based on your team's feedback about narrowing React effect dependencies to required fields.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/AddRepositoryModals/AddRepositoryModals.tsx, line 47:

<comment>The effect dependency list includes `folderImport.start`, which is not stable here and can repeatedly re-trigger `start()` after a single trigger bump. Depend only on `folderImportTrigger` for this counter-based pulse effect.

(Based on your team's feedback about narrowing React effect dependencies to required fields.) </comment>

<file context>
@@ -0,0 +1,76 @@
+		// identity changes every render (new hook instance per render) and
+		// we don't want to restart the flow on those changes.
+		// eslint-disable-next-line react-hooks/exhaustive-deps
+	}, [folderImportTrigger, folderImport.start]);
+
+	return (
</file context>
Fix with Cubic


return (
<>
<NewProjectModal
open={active.kind === "new-project"}
onOpenChange={(open) => {
if (!open) close();
}}
onSuccess={() => toast.success("Project created.")}
onError={(message) => toast.error(`Create failed: ${message}`)}
/>
<PinAndSetupModal
project={active.kind === "pin-and-setup" ? active.target : null}
forceRepoint={
active.kind === "pin-and-setup" ? active.forceRepoint : false
}
onOpenChange={(open) => {
if (!open) close();
}}
onSuccess={() => {
toast.success("Project pinned and set up.");
// Per-open one-shot callback (e.g. retry a pending workspace
// create that surfaced PROJECT_NOT_SETUP).
if (active.kind === "pin-and-setup") active.onSuccess?.();
}}
onError={(message) => toast.error(`Setup failed: ${message}`)}
/>
<FolderFirstImportModal
state={folderImport.state}
onCancel={folderImport.cancel}
onConfirmCreateAsNew={folderImport.confirmCreateAsNew}
onConfirmPickCandidate={folderImport.confirmPickCandidate}
onConfirmRepoint={folderImport.confirmRepoint}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AddRepositoryModals } from "./AddRepositoryModals";
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@superset/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { cn } from "@superset/ui/utils";
import { useMatchRoute, useNavigate } from "@tanstack/react-router";
import { LuFolderPlus, LuLayers, LuPlus } from "react-icons/lu";
import { HiMiniPlus } from "react-icons/hi2";
import { LuFolderInput, LuFolderPlus, LuLayers, LuPlus } from "react-icons/lu";
import { useHotkeyDisplay } from "renderer/hotkeys";
import { OrganizationDropdown } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown";
import { STROKE_WIDTH_THICK } from "renderer/screens/main/components/WorkspaceSidebar/constants";
import {
useOpenNewProjectModal,
useTriggerFolderImport,
} from "renderer/stores/add-repository-modal";
import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal";

interface DashboardSidebarHeaderProps {
Expand All @@ -15,6 +26,8 @@ export function DashboardSidebarHeader({
isCollapsed = false,
}: DashboardSidebarHeaderProps) {
const openModal = useOpenNewWorkspaceModal();
const openNewProject = useOpenNewProjectModal();
const triggerFolderImport = useTriggerFolderImport();
const shortcutText = useHotkeyDisplay("NEW_WORKSPACE").text;
const navigate = useNavigate();
const matchRoute = useMatchRoute();
Expand Down Expand Up @@ -47,17 +60,31 @@ export function DashboardSidebarHeader({
<TooltipContent side="right">Workspaces</TooltipContent>
</Tooltip>

<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<button
type="button"
className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
>
<LuFolderPlus className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">Add Repository</TooltipContent>
</Tooltip>
<DropdownMenu>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
>
<LuFolderPlus className="size-4" />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="right">Add repository</TooltipContent>
</Tooltip>
<DropdownMenuContent align="start">
<DropdownMenuItem onSelect={openNewProject}>
<HiMiniPlus className="size-4" />
New project
</DropdownMenuItem>
<DropdownMenuItem onSelect={triggerFolderImport}>
<LuFolderInput className="size-4" />
Import existing folder
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
Expand All @@ -83,17 +110,31 @@ export function DashboardSidebarHeader({
<div className="flex-1 min-w-0">
<OrganizationDropdown variant="expanded" />
</div>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<button
type="button"
className="flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
>
<LuFolderPlus className="size-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">Add Repository</TooltipContent>
</Tooltip>
<DropdownMenu>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
>
<LuFolderPlus className="size-4" />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="right">Add repository</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={openNewProject}>
<HiMiniPlus className="size-4" />
New project
</DropdownMenuItem>
<DropdownMenuItem onSelect={triggerFolderImport}>
<LuFolderInput className="size-4" />
Import existing folder
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type {
} from "@dnd-kit/core";
import { cn } from "@superset/ui/utils";
import { AnimatePresence, motion } from "framer-motion";
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import { useOpenPinAndSetupModal } from "renderer/stores/add-repository-modal";
import type { DashboardSidebarProject } from "../../types";
import { getProjectChildrenWorkspaces } from "../../utils/projectChildren";
import { DashboardSidebarCollapsedProjectContent } from "./components/DashboardSidebarCollapsedProjectContent";
Expand Down Expand Up @@ -60,6 +61,28 @@ export function DashboardSidebarProjectSection({

const totalWorkspaceCount = flattenedCollapsedWorkspaces.length;

// Phase 2: "Set up here" inline CTA for unbacked-on-this-host rows.
// Opens the same Pin & set up modal that the Available section uses,
// with the project pre-filled so the user only has to pick a folder.
// Phase 4: "Repair" reuses the same modal with forceRepoint so the
// user gets the destructive-confirmation copy immediately.
const openPinAndSetup = useOpenPinAndSetupModal();
const pinTarget = useMemo(
() => ({
id: project.id,
name: project.name,
githubOwner: project.githubOwner,
githubRepoName: project.githubRepoName,
}),
[project.githubOwner, project.githubRepoName, project.id, project.name],
);
const handleSetUpHere = useCallback(() => {
openPinAndSetup(pinTarget);
}, [openPinAndSetup, pinTarget]);
const handleRepairPath = useCallback(() => {
openPinAndSetup(pinTarget, { forceRepoint: true });
}, [openPinAndSetup, pinTarget]);

if (isSidebarCollapsed) {
return (
<DashboardSidebarProjectContextMenu
Expand All @@ -73,12 +96,15 @@ export function DashboardSidebarProjectSection({
<DashboardSidebarCollapsedProjectContent
projectName={project.name}
githubOwner={project.githubOwner}
backingState={project.backingState}
isCollapsed={project.isCollapsed}
totalWorkspaceCount={totalWorkspaceCount}
workspaces={flattenedCollapsedWorkspaces}
workspaceShortcutLabels={workspaceShortcutLabels}
onWorkspaceHover={onWorkspaceHover}
onToggleCollapse={() => onToggleCollapse(project.id)}
onSetUpHere={handleSetUpHere}
onRepairPath={handleRepairPath}
/>
</div>
</DashboardSidebarProjectContextMenu>
Expand All @@ -97,6 +123,7 @@ export function DashboardSidebarProjectSection({
<DashboardSidebarProjectRow
projectName={project.name}
githubOwner={project.githubOwner}
backingState={project.backingState}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WE should be able to remove this backing state as a concept from most places - instead, we can query if a host is online to see if the project is already set up in the new workspace creation modal and the eventual cron logic (which'll probably also share the project selector, so maybe we can consolidate some of this in a project selector component)?

totalWorkspaceCount={totalWorkspaceCount}
isCollapsed={project.isCollapsed}
isRenaming={isRenaming}
Expand All @@ -107,6 +134,8 @@ export function DashboardSidebarProjectSection({
onStartRename={startRename}
onToggleCollapse={() => onToggleCollapse(project.id)}
onNewWorkspace={handleNewWorkspace}
onSetUpHere={handleSetUpHere}
onRepairPath={handleRepairPath}
{...(dragHandleAttributes ?? {})}
{...(dragHandleListeners ?? {})}
/>
Expand Down
Loading