630 lines
20 KiB
TypeScript
630 lines
20 KiB
TypeScript
"use client";
|
|
|
|
import { FormEvent, useCallback, useState } from "react";
|
|
import type { JiraTicket } from "@/lib/types/jira";
|
|
import type { LinearIssue } from "@/lib/types/linear";
|
|
import type { Repository } from "@/lib/types/http";
|
|
import type { RepositoryBranchesState } from "@/lib/state/slices/workspace/types";
|
|
import { branchOptionValue } from "@/components/branch-picker-options";
|
|
import { SHORTCUTS } from "@/lib/keyboard/constants";
|
|
import { useIsUtilityConfigured } from "@/hooks/use-is-utility-configured";
|
|
import { useKeyboardShortcutHandler } from "@/hooks/use-keyboard-shortcut";
|
|
import { useUtilityAgentGenerator } from "@/hooks/use-utility-agent-generator";
|
|
import { usePromptResultDelivery } from "@/hooks/use-prompt-result-delivery";
|
|
import { useTaskSubmitHandlers } from "@/components/task-create-dialog-submit";
|
|
import { useToast } from "@/components/toast-provider";
|
|
import { useRepositorySets } from "@/hooks/domains/workspace/use-repository-sets";
|
|
import { useApplyRepositorySet } from "@/components/task-create-dialog-repository-sets-apply";
|
|
import { selectedRepositoryIdsForSet } from "@/components/task-create-dialog-repository-sets";
|
|
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
|
import {
|
|
useDialogFormState,
|
|
useTaskCreateDialogEffects,
|
|
useDialogHandlers,
|
|
useLockedFieldSync,
|
|
useSessionRepoName,
|
|
useTaskCreateDialogData,
|
|
computeIsTaskStarted,
|
|
type DialogFormState,
|
|
} from "@/components/task-create-dialog-state";
|
|
import type { TaskCreateDialogProps } from "@/components/task-create-dialog";
|
|
import { useResolvedTaskCreateWorkflowContext } from "@/components/task-create-dialog-workflow-context";
|
|
import { truncateRemoteTaskTitle } from "@/lib/task-title";
|
|
import { t } from "@/lib/i18n";
|
|
import { listRepositoryBranchPolicies } from "@/lib/api";
|
|
import { useTaskEditDialogDependencies } from "@/hooks/domains/task/use-task-edit-dialog-dependencies";
|
|
import {
|
|
buildWorkflowAgentOverrideValidation,
|
|
type WorkflowAgentOverrideValidation,
|
|
} from "@/components/task-create-dialog-workflow-agent-override-validation";
|
|
|
|
// Catalog key: module scope, so it is resolved at the call site.
|
|
const PROMPT_INSERTED_MESSAGE_KEY = "task:enhancedPromptInserted";
|
|
|
|
function useEnhanceForDialog(
|
|
fs: DialogFormState,
|
|
taskId: string | null | undefined,
|
|
open: boolean,
|
|
) {
|
|
const isConfigured = useIsUtilityConfigured();
|
|
const { toast } = useToast();
|
|
const { enhancePrompt, isEnhancingPrompt } = useUtilityAgentGenerator({
|
|
sessionId: null,
|
|
taskTitle: fs.taskName,
|
|
});
|
|
const applyDescription = useCallback(
|
|
(value: string) => {
|
|
const input = fs.descriptionInputRef.current;
|
|
if (!input) return false;
|
|
input.setValue(value);
|
|
const applied = input.getValue() === value;
|
|
if (applied) fs.setHasDescription(value.trim().length > 0);
|
|
return applied;
|
|
},
|
|
[fs],
|
|
);
|
|
const promptDelivery = usePromptResultDelivery({
|
|
scopeKey: `task-create:${open}:${fs.openCycle}:${taskId ?? ""}`,
|
|
getCurrent: () => fs.descriptionInputRef.current?.getValue() ?? null,
|
|
apply: applyDescription,
|
|
});
|
|
const onEnhance = useCallback(() => {
|
|
const current = fs.descriptionInputRef.current?.getValue() ?? "";
|
|
if (!current.trim()) return;
|
|
const generation = promptDelivery.captureScope();
|
|
void enhancePrompt(current, (result) => {
|
|
const inserted = promptDelivery.deliver(current, result, generation);
|
|
if (inserted) toast({ description: t(PROMPT_INSERTED_MESSAGE_KEY), variant: "success" });
|
|
return inserted;
|
|
});
|
|
}, [enhancePrompt, fs.descriptionInputRef, promptDelivery, toast]);
|
|
return {
|
|
onEnhance,
|
|
isLoading: isEnhancingPrompt,
|
|
isConfigured,
|
|
pendingResult: promptDelivery.pendingResult,
|
|
onApplyPending: promptDelivery.applyPending,
|
|
onCopyPending: promptDelivery.copyPending,
|
|
};
|
|
}
|
|
|
|
function useJiraImportHandler(fs: DialogFormState, handleTaskNameChange: (value: string) => void) {
|
|
return useCallback(
|
|
(ticket: JiraTicket) => {
|
|
handleTaskNameChange(truncateRemoteTaskTitle(`[${ticket.key}] ${ticket.summary}`));
|
|
const description = ticket.description?.trim()
|
|
? `${ticket.description}\n\n---\nJira: ${ticket.url}`
|
|
: `Jira: ${ticket.url}`;
|
|
fs.descriptionInputRef.current?.setValue(description);
|
|
fs.setHasDescription(true);
|
|
},
|
|
[fs, handleTaskNameChange],
|
|
);
|
|
}
|
|
|
|
function useLinearImportHandler(
|
|
fs: DialogFormState,
|
|
handleTaskNameChange: (value: string) => void,
|
|
) {
|
|
return useCallback(
|
|
(issue: LinearIssue) => {
|
|
handleTaskNameChange(truncateRemoteTaskTitle(`[${issue.identifier}] ${issue.title}`));
|
|
const description = issue.description?.trim()
|
|
? `${issue.description}\n\n---\nLinear: ${issue.url}`
|
|
: `Linear: ${issue.url}`;
|
|
fs.descriptionInputRef.current?.setValue(description);
|
|
fs.setHasDescription(true);
|
|
},
|
|
[fs, handleTaskNameChange],
|
|
);
|
|
}
|
|
|
|
function useEditDialogDependencies(
|
|
open: boolean,
|
|
isEditMode: boolean,
|
|
workspaceId: string | null | undefined,
|
|
taskId: string | null | undefined,
|
|
) {
|
|
return useTaskEditDialogDependencies({
|
|
open: open && isEditMode,
|
|
workspaceId,
|
|
taskId,
|
|
});
|
|
}
|
|
|
|
type SubmitWiringArgs = {
|
|
props: TaskCreateDialogProps;
|
|
fs: ReturnType<typeof useDialogFormState>;
|
|
computed: ReturnType<typeof useTaskCreateDialogData>["computed"];
|
|
workspaceRepositories: ReturnType<typeof useTaskCreateDialogData>["repositories"];
|
|
repositoryLocalPath: string;
|
|
isSessionMode: boolean;
|
|
isEditMode: boolean;
|
|
autoTitle: boolean;
|
|
editDependencies: ReturnType<typeof useTaskEditDialogDependencies>;
|
|
refreshBranchPolicies: () => Promise<void>;
|
|
preserveQueuedLastUsedOnClose: () => void;
|
|
workflowAgentOverridesBlockedReason?: string;
|
|
};
|
|
|
|
function useSubmitHandlersWiring({
|
|
props,
|
|
fs,
|
|
computed,
|
|
workspaceRepositories,
|
|
repositoryLocalPath,
|
|
isSessionMode,
|
|
isEditMode,
|
|
autoTitle,
|
|
editDependencies,
|
|
refreshBranchPolicies,
|
|
preserveQueuedLastUsedOnClose,
|
|
workflowAgentOverridesBlockedReason,
|
|
}: SubmitWiringArgs) {
|
|
const {
|
|
workspaceId,
|
|
workflowId,
|
|
editingTask,
|
|
onSuccess,
|
|
onCreateSession,
|
|
onOpenChange,
|
|
createTask,
|
|
} = props;
|
|
const { parentTaskId } = props;
|
|
const taskId = props.taskId ?? null;
|
|
return useTaskSubmitHandlers({
|
|
isSessionMode,
|
|
isEditMode,
|
|
autoTitle,
|
|
autopilot: fs.autopilot,
|
|
isPassthroughProfile: computed.isPassthroughProfile,
|
|
taskName: fs.taskName,
|
|
workspaceId,
|
|
workflowId,
|
|
effectiveWorkflowId: computed.effectiveWorkflowId,
|
|
repositories: fs.repositories,
|
|
repositoriesDirty: fs.repositoriesDirty,
|
|
discoveredRepositories: fs.discoveredRepositories,
|
|
workspaceRepositories,
|
|
useRemote: fs.useRemote,
|
|
remoteRepos: fs.remoteRepos,
|
|
prInfoByUrl: fs.prInfoByUrl,
|
|
agentProfileId: computed.effectiveAgentProfileId,
|
|
executorId: fs.executorId,
|
|
executorProfileId: fs.executorProfileId,
|
|
seededExecutorProfileId: fs.seededExecutorProfileId,
|
|
editingTask,
|
|
onSuccess,
|
|
onCreateSession,
|
|
onOpenChange,
|
|
createTask,
|
|
refreshBranchPolicies,
|
|
preserveTaskCreateLastUsedOnClose: preserveQueuedLastUsedOnClose,
|
|
taskId,
|
|
parentTaskId,
|
|
descriptionInputRef: fs.descriptionInputRef,
|
|
setIsCreatingSession: fs.setIsCreatingSession,
|
|
setIsCreatingTask: fs.setIsCreatingTask,
|
|
setHasTitle: fs.setHasTitle,
|
|
setHasDescription: fs.setHasDescription,
|
|
setTaskName: fs.setTaskName,
|
|
setRepositories: fs.setRepositories,
|
|
setRemoteRepos: fs.setRemoteRepos,
|
|
setAgentProfileId: fs.setAgentProfileId,
|
|
setExecutorId: fs.setExecutorId,
|
|
setSelectedWorkflowId: fs.setSelectedWorkflowId,
|
|
setFetchedSteps: fs.setFetchedSteps,
|
|
clearDraft: fs.clearDraft,
|
|
freshBranchEnabled: fs.freshBranchEnabled,
|
|
isLocalExecutor: computed.isLocalExecutor,
|
|
repositoryLocalPath,
|
|
noRepository: fs.noRepository,
|
|
workspacePath: fs.workspacePath,
|
|
priority: fs.priority,
|
|
workflowAgentOverrides: fs.workflowAgentOverrides,
|
|
workflowAgentOverridesBlockedReason,
|
|
blockedBy: fs.blockedBy,
|
|
editDependencies,
|
|
});
|
|
}
|
|
|
|
function resolveSingleRowLocalPath(fs: DialogFormState, repositories: Repository[]): string {
|
|
if (fs.repositories.length !== 1) return "";
|
|
const row = fs.repositories[0];
|
|
if (row.localPath) return row.localPath;
|
|
if (row.repositoryId)
|
|
return repositories.find((r) => r.id === row.repositoryId)?.local_path ?? "";
|
|
return "";
|
|
}
|
|
|
|
function resolveDialogMode(
|
|
mode: TaskCreateDialogProps["mode"],
|
|
editingTask: TaskCreateDialogProps["editingTask"],
|
|
) {
|
|
const isSessionMode = mode === "session";
|
|
const isEditMode = mode === "edit";
|
|
return {
|
|
isSessionMode,
|
|
isEditMode,
|
|
isTaskStarted: computeIsTaskStarted(isEditMode, editingTask),
|
|
};
|
|
}
|
|
|
|
function canUseFreshBranch(fs: DialogFormState, isLocalExecutor: boolean): boolean {
|
|
return !fs.useRemote && isLocalExecutor && fs.repositories.length === 1;
|
|
}
|
|
|
|
export function hasUnavailableSavedBase(
|
|
rows: DialogFormState["repositories"],
|
|
repositoryBranches: RepositoryBranchesState,
|
|
): boolean {
|
|
return rows.some((row) => {
|
|
if (!row.repositoryId || !row.baseBranch) return false;
|
|
if (!repositoryBranches.loadedByRepositoryId[row.repositoryId]) return false;
|
|
const branches = repositoryBranches.itemsByRepositoryId[row.repositoryId] ?? [];
|
|
return !branches.some((branch) => branchOptionValue(branch) === row.baseBranch);
|
|
});
|
|
}
|
|
|
|
function hasPendingSavedBaseValidation(
|
|
rows: DialogFormState["repositories"],
|
|
repositoryBranches: RepositoryBranchesState,
|
|
): boolean {
|
|
return rows.some(
|
|
(row) =>
|
|
Boolean(row.repositoryId && row.baseBranch) &&
|
|
!repositoryBranches.loadedByRepositoryId[row.repositoryId as string],
|
|
);
|
|
}
|
|
|
|
function savedBaseSubmitBlockedReason(
|
|
hasPendingValidation: boolean,
|
|
hasUnavailableBase: boolean,
|
|
): string | null {
|
|
if (hasPendingValidation) return t("task:loadingBranches2");
|
|
if (hasUnavailableBase) return t("task:repositorySetBaseUnavailable");
|
|
return null;
|
|
}
|
|
|
|
function savedBaseSubmitBlockedReasonForRows(
|
|
rows: DialogFormState["repositories"],
|
|
repositoryBranches: RepositoryBranchesState,
|
|
): string | null {
|
|
return savedBaseSubmitBlockedReason(
|
|
hasPendingSavedBaseValidation(rows, repositoryBranches),
|
|
hasUnavailableSavedBase(rows, repositoryBranches),
|
|
);
|
|
}
|
|
|
|
function useRefreshBranchPolicies(fs: DialogFormState) {
|
|
const storeApi = useAppStoreApi();
|
|
const setRepositoryBranchPolicies = useAppStore((state) => state.setRepositoryBranchPolicies);
|
|
const setRepositoryBranchPoliciesLoading = useAppStore(
|
|
(state) => state.setRepositoryBranchPoliciesLoading,
|
|
);
|
|
return useCallback(async () => {
|
|
const repositoryIds = [
|
|
...new Set(
|
|
fs.repositories
|
|
.map((row) => row.repositoryId)
|
|
.filter((repositoryId): repositoryId is string => Boolean(repositoryId)),
|
|
),
|
|
];
|
|
await Promise.all(
|
|
repositoryIds.map(async (repositoryId) => {
|
|
const requestRevision =
|
|
storeApi.getState().repositoryBranchPolicies.revisionByRepositoryId[repositoryId] ?? 0;
|
|
setRepositoryBranchPoliciesLoading(repositoryId, true);
|
|
try {
|
|
const response = await listRepositoryBranchPolicies(repositoryId, { cache: "no-store" });
|
|
setRepositoryBranchPolicies(
|
|
repositoryId,
|
|
response.repository_branch_policies,
|
|
requestRevision,
|
|
);
|
|
} catch {
|
|
// Keep the original task error visible when recovery cannot refresh.
|
|
} finally {
|
|
setRepositoryBranchPoliciesLoading(repositoryId, false);
|
|
}
|
|
}),
|
|
);
|
|
}, [fs.repositories, setRepositoryBranchPolicies, setRepositoryBranchPoliciesLoading, storeApi]);
|
|
}
|
|
|
|
function useDialogSetupData(
|
|
props: TaskCreateDialogProps,
|
|
fs: ReturnType<typeof useDialogFormState>,
|
|
) {
|
|
const { open, workspaceId, workflowId, defaultStepId, initialValues } = props;
|
|
const { toast } = useToast();
|
|
const upsertWorkspaceRepository = useAppStore((state) => state.upsertRepository);
|
|
const repositoryBranches = useAppStore((state) => state.repositoryBranches);
|
|
const data = useTaskCreateDialogData({
|
|
open,
|
|
workspaceId,
|
|
workflowId,
|
|
defaultStepId,
|
|
fs,
|
|
lockedWorkflow: props.lockedFields?.workflow === true,
|
|
agentProfileRecentUseContext:
|
|
props.mode === "session" || props.mode === "edit" ? "task_session" : "task_create",
|
|
});
|
|
const {
|
|
workflows,
|
|
agentProfiles,
|
|
executors,
|
|
repositories,
|
|
repositoriesLoading,
|
|
taskCreateLastUsed,
|
|
userSettingsLoaded,
|
|
computed,
|
|
} = data;
|
|
useTaskCreateDialogEffects(fs, {
|
|
open,
|
|
workspaceId,
|
|
workflowId,
|
|
effectiveWorkflowId: computed.effectiveWorkflowId,
|
|
repositories,
|
|
repositoriesLoading,
|
|
agentProfiles,
|
|
compatibleAgentProfiles: computed.compatibleAgentProfiles,
|
|
authLoaded: computed.authLoaded,
|
|
executors,
|
|
workspaceDefaults: computed.workspaceDefaults,
|
|
toast,
|
|
workflows,
|
|
isLocalExecutor: computed.isLocalExecutor,
|
|
lastUsedRepositoryId: taskCreateLastUsed.repositoryId,
|
|
userSettingsLoaded,
|
|
lastUsedAgentProfileId: taskCreateLastUsed.agentProfileId,
|
|
lastUsedExecutorProfileId: taskCreateLastUsed.executorProfileId,
|
|
lastUsedBranch: taskCreateLastUsed.branch,
|
|
preserveBranch: initialValues?.checkoutBranch || initialValues?.branch,
|
|
editingTaskExecutorProfileId: props.editingTask?.primaryExecutorProfileId,
|
|
});
|
|
useLockedFieldSync(open, workflowId, initialValues, fs, props.lockedFields?.workflow === true);
|
|
const handlers = useDialogHandlers(fs, repositories, {
|
|
workspaceId,
|
|
executors,
|
|
upsertWorkspaceRepository,
|
|
});
|
|
const savedBaseSubmitBlockedReasonValue = savedBaseSubmitBlockedReasonForRows(
|
|
fs.repositories,
|
|
repositoryBranches,
|
|
);
|
|
const refreshBranchPolicies = useRefreshBranchPolicies(fs);
|
|
return {
|
|
...data,
|
|
handlers,
|
|
refreshBranchPolicies,
|
|
repositoryLocalPath: resolveSingleRowLocalPath(fs, repositories),
|
|
savedBaseSubmitBlockedReason: savedBaseSubmitBlockedReasonValue,
|
|
};
|
|
}
|
|
|
|
function resolveWorkflowAgentOverrideValidation(
|
|
mode: TaskCreateDialogProps["mode"],
|
|
workspaceId: string | null | undefined,
|
|
fs: ReturnType<typeof useDialogFormState>,
|
|
data: ReturnType<typeof useDialogSetupData>,
|
|
): WorkflowAgentOverrideValidation {
|
|
return buildWorkflowAgentOverrideValidation({
|
|
effectiveWorkflowId: data.computed.effectiveWorkflowId,
|
|
snapshots: data.snapshots,
|
|
workspaceSnapshotRead: data.workspaceSnapshotRead,
|
|
workspaceId,
|
|
profiles: data.agentProfiles,
|
|
replacementOptions: data.computed.agentProfileOptions,
|
|
overrides: fs.workflowAgentOverrides,
|
|
isCreateMode: mode === "create",
|
|
});
|
|
}
|
|
|
|
export function useTaskCreateDialogSetup(
|
|
props: TaskCreateDialogProps,
|
|
options: { preserveQueuedLastUsedOnClose?: () => void } = {},
|
|
) {
|
|
const resolvedProps = useResolvedTaskCreateWorkflowContext(props);
|
|
const {
|
|
open,
|
|
mode = "create",
|
|
workspaceId,
|
|
workflowId,
|
|
editingTask,
|
|
initialValues,
|
|
} = resolvedProps;
|
|
const { isSessionMode, isEditMode, isTaskStarted } = resolveDialogMode(mode, editingTask);
|
|
const agentGeneratedTaskTitles = useAppStore(
|
|
(state) => state.userSettings.agentGeneratedTaskTitles,
|
|
);
|
|
const autoTitle = mode === "create" && agentGeneratedTaskTitles;
|
|
const fs = useDialogFormState(
|
|
open,
|
|
workspaceId,
|
|
workflowId,
|
|
initialValues,
|
|
resolvedProps.lockedFields?.workflow === true,
|
|
);
|
|
const editDependencies = useEditDialogDependencies(
|
|
open,
|
|
isEditMode,
|
|
workspaceId,
|
|
editingTask?.id ?? null,
|
|
);
|
|
const sessionRepoName = useSessionRepoName(isSessionMode);
|
|
const data = useDialogSetupData(resolvedProps, fs);
|
|
const { computed, repositoryLocalPath, refreshBranchPolicies, savedBaseSubmitBlockedReason } =
|
|
data;
|
|
const workflowAgentOverrideValidation = resolveWorkflowAgentOverrideValidation(
|
|
mode,
|
|
workspaceId,
|
|
fs,
|
|
data,
|
|
);
|
|
const workflowAgentOverridesBlockedReason =
|
|
mode === "create" ? workflowAgentOverrideValidation.blockedReason : undefined;
|
|
const submitHandlers = useSubmitHandlersWiring({
|
|
props: resolvedProps,
|
|
fs,
|
|
computed,
|
|
workspaceRepositories: data.repositories,
|
|
repositoryLocalPath,
|
|
isSessionMode,
|
|
isEditMode,
|
|
autoTitle,
|
|
editDependencies,
|
|
refreshBranchPolicies,
|
|
preserveQueuedLastUsedOnClose: options.preserveQueuedLastUsedOnClose ?? (() => undefined),
|
|
workflowAgentOverridesBlockedReason,
|
|
});
|
|
const { guardedHandleSubmit, handleKeyDown } = useDialogSubmitShortcut(
|
|
submitHandlers.handleSubmit,
|
|
resolvedProps.submitBlockedReason ??
|
|
savedBaseSubmitBlockedReason ??
|
|
workflowAgentOverridesBlockedReason,
|
|
!isTaskStarted && computed.noCompatibleAgent,
|
|
);
|
|
const enhance = useEnhanceForDialog(fs, resolvedProps.taskId, resolvedProps.open);
|
|
const handleJiraImport = useJiraImportHandler(fs, data.handlers.handleTaskNameChange);
|
|
const handleLinearImport = useLinearImportHandler(fs, data.handlers.handleTaskNameChange);
|
|
const freshBranchAvailable = canUseFreshBranch(fs, computed.isLocalExecutor);
|
|
const repositorySets = useDialogRepositorySets(
|
|
resolvedProps,
|
|
fs,
|
|
data.repositories,
|
|
computed,
|
|
data.userSettingsLoaded,
|
|
);
|
|
return {
|
|
...data,
|
|
fs,
|
|
isSessionMode,
|
|
isEditMode,
|
|
isCreateMode: mode === "create",
|
|
autoTitle,
|
|
isTaskStarted,
|
|
sessionRepoName,
|
|
computed,
|
|
submitHandlers,
|
|
handleKeyDown,
|
|
freshBranchAvailable,
|
|
repositorySets,
|
|
guardedHandleSubmit,
|
|
enhance,
|
|
handleJiraImport,
|
|
handleLinearImport,
|
|
editDependencies,
|
|
savedBaseSubmitBlockedReason,
|
|
workflowAgentOverrideValidation,
|
|
};
|
|
}
|
|
|
|
function useDialogRepositorySets(
|
|
resolvedProps: TaskCreateDialogProps,
|
|
fs: DialogFormState,
|
|
repositories: Repository[],
|
|
computed: ReturnType<typeof useTaskCreateDialogData>["computed"],
|
|
userSettingsLoaded: boolean,
|
|
) {
|
|
return useRepositorySetsForDialog({
|
|
workspaceId: resolvedProps.workspaceId ?? null,
|
|
open: resolvedProps.open,
|
|
rows: fs.repositories,
|
|
repositories,
|
|
setRepositories: fs.setRepositories,
|
|
setRepositoriesDirty: fs.setRepositoriesDirty,
|
|
userSettingsLoaded,
|
|
isLocalExecutor: computed.isLocalExecutor,
|
|
freshBranchEnabled: fs.freshBranchEnabled,
|
|
});
|
|
}
|
|
|
|
type RepositorySetsForDialogArgs = {
|
|
workspaceId: string | null;
|
|
open: boolean;
|
|
rows: DialogFormState["repositories"];
|
|
repositories: Repository[];
|
|
setRepositories: DialogFormState["setRepositories"];
|
|
setRepositoriesDirty: DialogFormState["setRepositoriesDirty"];
|
|
userSettingsLoaded: boolean;
|
|
isLocalExecutor: boolean;
|
|
freshBranchEnabled: boolean;
|
|
};
|
|
|
|
/**
|
|
* Assembles the repository-set props the picker needs: the workspace's sets, why
|
|
* applying one is unavailable, and the apply handler.
|
|
*
|
|
* Gated on `userSettingsLoaded` because the repository auto-select effect writes
|
|
* rows again once user settings arrive; offering the control before then lets a
|
|
* user apply a set that autopick immediately overwrites.
|
|
*/
|
|
function useRepositorySetsForDialog({
|
|
workspaceId,
|
|
open,
|
|
rows,
|
|
repositories,
|
|
setRepositories,
|
|
setRepositoriesDirty,
|
|
userSettingsLoaded,
|
|
isLocalExecutor,
|
|
freshBranchEnabled,
|
|
}: RepositorySetsForDialogArgs) {
|
|
const { sets } = useRepositorySets(workspaceId, open);
|
|
const onApply = useApplyRepositorySet({
|
|
rows,
|
|
repositories,
|
|
setRepositories,
|
|
setRepositoriesDirty,
|
|
});
|
|
const [saveOpen, setSaveOpen] = useState(false);
|
|
// Offer "Save as set" only when there is a workspace-repository selection worth
|
|
// saving, so the action is never a dead end.
|
|
const canSave = Boolean(workspaceId) && selectedRepositoryIdsForSet(rows).length > 0;
|
|
if (!userSettingsLoaded) return undefined;
|
|
return {
|
|
sets,
|
|
onApply,
|
|
save:
|
|
canSave && workspaceId
|
|
? {
|
|
workspaceId,
|
|
rows,
|
|
repositories,
|
|
isLocalExecutor,
|
|
freshBranchEnabled,
|
|
open: saveOpen,
|
|
setOpen: setSaveOpen,
|
|
}
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function useGuardedSubmit(
|
|
handleSubmit: (e: FormEvent) => void,
|
|
blockedReason: string | null | undefined,
|
|
compatibilityBlocked: boolean,
|
|
) {
|
|
const blocked = Boolean(blockedReason) || compatibilityBlocked;
|
|
return useCallback(
|
|
(e: FormEvent) => {
|
|
if (blocked) e.preventDefault();
|
|
else handleSubmit(e);
|
|
},
|
|
[blocked, handleSubmit],
|
|
);
|
|
}
|
|
|
|
function useDialogSubmitShortcut(
|
|
handleSubmit: ReturnType<typeof useSubmitHandlersWiring>["handleSubmit"],
|
|
blockedReason: string | null | undefined,
|
|
compatibilityBlocked: boolean,
|
|
) {
|
|
const guardedHandleSubmit = useGuardedSubmit(handleSubmit, blockedReason, compatibilityBlocked);
|
|
const handleKeyDown = useKeyboardShortcutHandler(SHORTCUTS.SUBMIT, (event) => {
|
|
guardedHandleSubmit(event as unknown as FormEvent);
|
|
});
|
|
return { guardedHandleSubmit, handleKeyDown };
|
|
}
|