mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-04 20:05:38 +08:00
refactor: modularize project services, and wizard create/clone flow
Restructure project creation, listing, GitHub clone progress, and TaskMaster details behind a dedicated TypeScript module under server/modules/projects/, and align the client wizard with a single path-based flow. Server / routing - Remove server/routes/projects.js and mount server/modules/projects/ projects.routes.ts at /api/projects (still behind authenticateToken). - Drop duplicate handlers from server/index.js for GET /api/projects and GET /api/projects/:projectId/taskmaster; those live on the new router. - Import WORKSPACES_ROOT and validateWorkspacePath from shared utils in index.js instead of the deleted projects route module. Projects router (projects.routes.ts) - GET /: list projects with sessions (existing snapshot behavior). - POST /create-project: validate body, reject legacy workspaceType and mixed clone fields, delegate to createProject service, return distinct success copy when an archived path is reactivated. - GET /clone-progress: Server-Sent Events for clone progress/complete/error; requires authenticated user id for token resolution; wires startCloneProject. - GET /:projectId/taskmaster: delegates to getProjectTaskMaster. Services (new) - project-management.service.ts: path validation, workspace directory creation, persistence via projectsDb.createProjectPath, mapping to API project shape; surfaces AppError for validation, conflict, and not-found cases; optional dependency injection for tests. - project-clone.service.ts: validates workspace, resolves GitHub auth (stored token or inline token), runs git clone with progress callbacks, registers project via createProject on success; sanitizes errors and supports cancellation; injectable dependencies for tests. - projects-has-taskmaster.service.ts: moves TaskMaster detection and normalization out of server/projects.js; resolve-by-id and public getProjectTaskMaster with structured AppError responses. Persistence and shared types - projectsDb.createProjectPath now returns CreateProjectPathResult (created | reactivated_archived | active_conflict) using INSERT … ON CONFLICT with selective update when the row is archived; normalizes display name from path or custom name; repository row typing moves to shared ProjectRepositoryRow. - getProjectPaths() returns only non-archived rows (isArchived = 0). - shared/types.ts: ProjectRepositoryRow, CreateProjectPathResult/outcome, WorkspacePathValidationResult. - shared/utils.ts: WORKSPACES_ROOT, forbidden path lists, validateWorkspacePath, asyncHandler for Express async routes. Legacy cleanup - server/projects.js: remove detectTaskMasterFolder, normalizeTaskMasterInfo, and getProjectTaskMasterById (logic lives in the new service). - server/routes/agent.js: register external API project paths with projectsDb.createProjectPath instead of addProjectManually try/catch; treat active_conflict as an existing registration and continue. Tests - Add Node test suites for project-management, project-clone, and projects-has-taskmaster services; update projects.service test import for renamed projects-with-sessions-fetch.service.ts. Rename - projects.service.ts → projects-with-sessions-fetch.service.ts; re-export from modules/projects/index.ts. Client (project creation wizard) - Remove StepTypeSelection and workspaceType from form state and types; wizard is two steps (configure path/GitHub auth, then review). - createWorkspaceRequest → createProjectRequest; clone vs create-only inferred from githubUrl (pathUtils / isCloneWorkflow). - Adjust step indices, WizardProgress, StepConfiguration/Review, WorkspacePathField, and src/utils/api.js as needed for the new API. Docs - Minor websocket README touch-up. Net: ~1.6k insertions / ~0.9k deletions across 29 files; behavior is centralized in typed services with explicit HTTP errors and test seams.
This commit is contained in:
@@ -4,13 +4,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import ErrorBanner from './components/ErrorBanner';
|
||||
import StepConfiguration from './components/StepConfiguration';
|
||||
import StepReview from './components/StepReview';
|
||||
import StepTypeSelection from './components/StepTypeSelection';
|
||||
import WizardFooter from './components/WizardFooter';
|
||||
import WizardProgress from './components/WizardProgress';
|
||||
import { useGithubTokens } from './hooks/useGithubTokens';
|
||||
import { cloneWorkspaceWithProgress, createWorkspaceRequest } from './data/workspaceApi';
|
||||
import { cloneWorkspaceWithProgress, createProjectRequest } from './data/workspaceApi';
|
||||
import { isCloneWorkflow, shouldShowGithubAuthentication } from './utils/pathUtils';
|
||||
import type { TokenMode, WizardFormState, WizardStep, WorkspaceType } from './types';
|
||||
import type { TokenMode, WizardFormState, WizardStep } from './types';
|
||||
|
||||
type ProjectCreationWizardProps = {
|
||||
onClose: () => void;
|
||||
@@ -18,7 +17,6 @@ type ProjectCreationWizardProps = {
|
||||
};
|
||||
|
||||
const initialFormState: WizardFormState = {
|
||||
workspaceType: 'existing',
|
||||
workspacePath: '',
|
||||
githubUrl: '',
|
||||
tokenMode: 'stored',
|
||||
@@ -38,7 +36,7 @@ export default function ProjectCreationWizard({
|
||||
const [cloneProgress, setCloneProgress] = useState('');
|
||||
|
||||
const shouldLoadTokens =
|
||||
step === 2 && shouldShowGithubAuthentication(formState.workspaceType, formState.githubUrl);
|
||||
step === 1 && shouldShowGithubAuthentication(formState.githubUrl);
|
||||
|
||||
const autoSelectToken = useCallback((tokenId: string) => {
|
||||
setFormState((previous) => ({ ...previous, selectedGithubToken: tokenId }));
|
||||
@@ -60,11 +58,6 @@ export default function ProjectCreationWizard({
|
||||
setFormState((previous) => ({ ...previous, [key]: value }));
|
||||
}, []);
|
||||
|
||||
const updateWorkspaceType = useCallback(
|
||||
(workspaceType: WorkspaceType) => updateField('workspaceType', workspaceType),
|
||||
[updateField],
|
||||
);
|
||||
|
||||
const updateTokenMode = useCallback(
|
||||
(tokenMode: TokenMode) => updateField('tokenMode', tokenMode),
|
||||
[updateField],
|
||||
@@ -74,22 +67,13 @@ export default function ProjectCreationWizard({
|
||||
setError(null);
|
||||
|
||||
if (step === 1) {
|
||||
if (!formState.workspaceType) {
|
||||
setError(t('projectWizard.errors.selectType'));
|
||||
return;
|
||||
}
|
||||
setStep(2);
|
||||
return;
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
if (!formState.workspacePath.trim()) {
|
||||
setError(t('projectWizard.errors.providePath'));
|
||||
return;
|
||||
}
|
||||
setStep(3);
|
||||
setStep(2);
|
||||
}
|
||||
}, [formState.workspacePath, formState.workspaceType, step, t]);
|
||||
}, [formState.workspacePath, step, t]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
setError(null);
|
||||
@@ -102,7 +86,7 @@ export default function ProjectCreationWizard({
|
||||
setCloneProgress('');
|
||||
|
||||
try {
|
||||
const shouldCloneRepository = isCloneWorkflow(formState.workspaceType, formState.githubUrl);
|
||||
const shouldCloneRepository = isCloneWorkflow(formState.githubUrl);
|
||||
|
||||
if (shouldCloneRepository) {
|
||||
const project = await cloneWorkspaceWithProgress(
|
||||
@@ -123,8 +107,7 @@ export default function ProjectCreationWizard({
|
||||
return;
|
||||
}
|
||||
|
||||
const project = await createWorkspaceRequest({
|
||||
workspaceType: formState.workspaceType,
|
||||
const project = await createProjectRequest({
|
||||
path: formState.workspacePath.trim(),
|
||||
});
|
||||
|
||||
@@ -142,8 +125,8 @@ export default function ProjectCreationWizard({
|
||||
}, [formState, onClose, onProjectCreated, t]);
|
||||
|
||||
const shouldCloneRepository = useMemo(
|
||||
() => isCloneWorkflow(formState.workspaceType, formState.githubUrl),
|
||||
[formState.githubUrl, formState.workspaceType],
|
||||
() => isCloneWorkflow(formState.githubUrl),
|
||||
[formState.githubUrl],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -173,15 +156,7 @@ export default function ProjectCreationWizard({
|
||||
{error && <ErrorBanner message={error} />}
|
||||
|
||||
{step === 1 && (
|
||||
<StepTypeSelection
|
||||
workspaceType={formState.workspaceType}
|
||||
onWorkspaceTypeChange={updateWorkspaceType}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<StepConfiguration
|
||||
workspaceType={formState.workspaceType}
|
||||
workspacePath={formState.workspacePath}
|
||||
githubUrl={formState.githubUrl}
|
||||
tokenMode={formState.tokenMode}
|
||||
@@ -200,11 +175,11 @@ export default function ProjectCreationWizard({
|
||||
onNewGithubTokenChange={(newGithubToken) =>
|
||||
updateField('newGithubToken', newGithubToken)
|
||||
}
|
||||
onAdvanceToConfirm={() => setStep(3)}
|
||||
onAdvanceToConfirm={() => setStep(2)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
{step === 2 && (
|
||||
<StepReview
|
||||
formState={formState}
|
||||
selectedTokenName={selectedTokenName}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '../../../shared/view/ui';
|
||||
import { shouldShowGithubAuthentication } from '../utils/pathUtils';
|
||||
import type { GithubTokenCredential, TokenMode, WorkspaceType } from '../types';
|
||||
import type { GithubTokenCredential, TokenMode } from '../types';
|
||||
import GithubAuthenticationCard from './GithubAuthenticationCard';
|
||||
import WorkspacePathField from './WorkspacePathField';
|
||||
|
||||
type StepConfigurationProps = {
|
||||
workspaceType: WorkspaceType;
|
||||
workspacePath: string;
|
||||
githubUrl: string;
|
||||
tokenMode: TokenMode;
|
||||
@@ -25,7 +24,6 @@ type StepConfigurationProps = {
|
||||
};
|
||||
|
||||
export default function StepConfiguration({
|
||||
workspaceType,
|
||||
workspacePath,
|
||||
githubUrl,
|
||||
tokenMode,
|
||||
@@ -43,19 +41,16 @@ export default function StepConfiguration({
|
||||
onAdvanceToConfirm,
|
||||
}: StepConfigurationProps) {
|
||||
const { t } = useTranslation();
|
||||
const showGithubAuth = shouldShowGithubAuthentication(workspaceType, githubUrl);
|
||||
const showGithubAuth = shouldShowGithubAuthentication(githubUrl);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{workspaceType === 'existing'
|
||||
? t('projectWizard.step2.existingPath')
|
||||
: t('projectWizard.step2.newPath')}
|
||||
{t('projectWizard.step2.newPath')}
|
||||
</label>
|
||||
|
||||
<WorkspacePathField
|
||||
workspaceType={workspaceType}
|
||||
value={workspacePath}
|
||||
disabled={isCreating}
|
||||
onChange={onWorkspacePathChange}
|
||||
@@ -63,45 +58,39 @@ export default function StepConfiguration({
|
||||
/>
|
||||
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{workspaceType === 'existing'
|
||||
? t('projectWizard.step2.existingHelp')
|
||||
: t('projectWizard.step2.newHelp')}
|
||||
{t('projectWizard.step2.newHelp')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{workspaceType === 'new' && (
|
||||
<>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t('projectWizard.step2.githubUrl')}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={githubUrl}
|
||||
onChange={(event) => onGithubUrlChange(event.target.value)}
|
||||
placeholder="https://github.com/username/repository"
|
||||
className="w-full"
|
||||
disabled={isCreating}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('projectWizard.step2.githubHelp')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t('projectWizard.step2.githubUrl')}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={githubUrl}
|
||||
onChange={(event) => onGithubUrlChange(event.target.value)}
|
||||
placeholder="https://github.com/username/repository"
|
||||
className="w-full"
|
||||
disabled={isCreating}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t('projectWizard.step2.githubHelp')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showGithubAuth && (
|
||||
<GithubAuthenticationCard
|
||||
tokenMode={tokenMode}
|
||||
selectedGithubToken={selectedGithubToken}
|
||||
newGithubToken={newGithubToken}
|
||||
availableTokens={availableTokens}
|
||||
loadingTokens={loadingTokens}
|
||||
tokenLoadError={tokenLoadError}
|
||||
onTokenModeChange={onTokenModeChange}
|
||||
onSelectedGithubTokenChange={onSelectedGithubTokenChange}
|
||||
onNewGithubTokenChange={onNewGithubTokenChange}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{showGithubAuth && (
|
||||
<GithubAuthenticationCard
|
||||
tokenMode={tokenMode}
|
||||
selectedGithubToken={selectedGithubToken}
|
||||
newGithubToken={newGithubToken}
|
||||
availableTokens={availableTokens}
|
||||
loadingTokens={loadingTokens}
|
||||
tokenLoadError={tokenLoadError}
|
||||
onTokenModeChange={onTokenModeChange}
|
||||
onSelectedGithubTokenChange={onSelectedGithubTokenChange}
|
||||
onNewGithubTokenChange={onNewGithubTokenChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,17 +42,6 @@ export default function StepReview({
|
||||
</h4>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
{t('projectWizard.step3.workspaceType')}
|
||||
</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{formState.workspaceType === 'existing'
|
||||
? t('projectWizard.step3.existingWorkspace')
|
||||
: t('projectWizard.step3.newWorkspace')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">{t('projectWizard.step3.path')}</span>
|
||||
<span className="break-all font-mono text-xs text-gray-900 dark:text-white">
|
||||
@@ -60,7 +49,7 @@ export default function StepReview({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{formState.workspaceType === 'new' && formState.githubUrl && (
|
||||
{formState.githubUrl && (
|
||||
<>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600 dark:text-gray-400">
|
||||
@@ -94,11 +83,9 @@ export default function StepReview({
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
{formState.workspaceType === 'existing'
|
||||
? t('projectWizard.step3.existingInfo')
|
||||
: formState.githubUrl
|
||||
? t('projectWizard.step3.newWithClone')
|
||||
: t('projectWizard.step3.newEmpty')}
|
||||
{formState.githubUrl
|
||||
? t('projectWizard.step3.newWithClone')
|
||||
: t('projectWizard.step3.newEmpty')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { FolderPlus, GitBranch } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { WorkspaceType } from '../types';
|
||||
|
||||
type StepTypeSelectionProps = {
|
||||
workspaceType: WorkspaceType;
|
||||
onWorkspaceTypeChange: (workspaceType: WorkspaceType) => void;
|
||||
};
|
||||
|
||||
export default function StepTypeSelection({
|
||||
workspaceType,
|
||||
onWorkspaceTypeChange,
|
||||
}: StepTypeSelectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h4 className="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{t('projectWizard.step1.question')}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<button
|
||||
onClick={() => onWorkspaceTypeChange('existing')}
|
||||
className={`rounded-lg border-2 p-4 text-left transition-all ${
|
||||
workspaceType === 'existing'
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-green-100 dark:bg-green-900/50">
|
||||
<FolderPlus className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h5 className="mb-1 font-semibold text-gray-900 dark:text-white">
|
||||
{t('projectWizard.step1.existing.title')}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('projectWizard.step1.existing.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onWorkspaceTypeChange('new')}
|
||||
className={`rounded-lg border-2 p-4 text-left transition-all ${
|
||||
workspaceType === 'new'
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-purple-100 dark:bg-purple-900/50">
|
||||
<GitBranch className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h5 className="mb-1 font-semibold text-gray-900 dark:text-white">
|
||||
{t('projectWizard.step1.new.title')}
|
||||
</h5>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('projectWizard.step1.new.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export default function WizardFooter({
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button onClick={step === 3 ? onCreate : onNext} disabled={isCreating}>
|
||||
<Button onClick={step === 2 ? onCreate : onNext} disabled={isCreating}>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
@@ -45,7 +45,7 @@ export default function WizardFooter({
|
||||
? t('projectWizard.buttons.cloning', { defaultValue: 'Cloning...' })
|
||||
: t('projectWizard.buttons.creating')}
|
||||
</>
|
||||
) : step === 3 ? (
|
||||
) : step === 2 ? (
|
||||
<>
|
||||
<Check className="mr-1 h-4 w-4" />
|
||||
{t('projectWizard.buttons.createProject')}
|
||||
|
||||
@@ -9,7 +9,7 @@ type WizardProgressProps = {
|
||||
|
||||
export default function WizardProgress({ step }: WizardProgressProps) {
|
||||
const { t } = useTranslation();
|
||||
const steps: WizardStep[] = [1, 2, 3];
|
||||
const steps: WizardStep[] = [1, 2];
|
||||
|
||||
return (
|
||||
<div className="px-6 pb-2 pt-4">
|
||||
@@ -30,14 +30,12 @@ export default function WizardProgress({ step }: WizardProgressProps) {
|
||||
</div>
|
||||
<span className="hidden text-sm font-medium text-gray-700 dark:text-gray-300 sm:inline">
|
||||
{currentStep === 1
|
||||
? t('projectWizard.steps.type')
|
||||
: currentStep === 2
|
||||
? t('projectWizard.steps.configure')
|
||||
: t('projectWizard.steps.confirm')}
|
||||
? t('projectWizard.steps.configure')
|
||||
: t('projectWizard.steps.confirm')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{currentStep < 3 && (
|
||||
{currentStep < 2 && (
|
||||
<div
|
||||
className={`mx-2 h-1 flex-1 rounded ${
|
||||
currentStep < step ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700'
|
||||
|
||||
@@ -3,11 +3,10 @@ import { FolderOpen } from 'lucide-react';
|
||||
import { Button, Input } from '../../../shared/view/ui';
|
||||
import { browseFilesystemFolders } from '../data/workspaceApi';
|
||||
import { getSuggestionRootPath } from '../utils/pathUtils';
|
||||
import type { FolderSuggestion, WorkspaceType } from '../types';
|
||||
import type { FolderSuggestion } from '../types';
|
||||
import FolderBrowserModal from './FolderBrowserModal';
|
||||
|
||||
type WorkspacePathFieldProps = {
|
||||
workspaceType: WorkspaceType;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
onChange: (path: string) => void;
|
||||
@@ -15,7 +14,6 @@ type WorkspacePathFieldProps = {
|
||||
};
|
||||
|
||||
export default function WorkspacePathField({
|
||||
workspaceType,
|
||||
value,
|
||||
disabled = false,
|
||||
onChange,
|
||||
@@ -88,11 +86,7 @@ export default function WorkspacePathField({
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
placeholder={
|
||||
workspaceType === 'existing'
|
||||
? '/path/to/existing/workspace'
|
||||
: '/path/to/new/workspace'
|
||||
}
|
||||
placeholder="/path/to/project/workspace"
|
||||
className="w-full"
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -127,7 +121,7 @@ export default function WorkspacePathField({
|
||||
|
||||
<FolderBrowserModal
|
||||
isOpen={showFolderBrowser}
|
||||
autoAdvanceOnSelect={workspaceType === 'existing'}
|
||||
autoAdvanceOnSelect={false}
|
||||
onClose={() => setShowFolderBrowser(false)}
|
||||
onFolderSelected={handleFolderSelected}
|
||||
/>
|
||||
|
||||
@@ -3,8 +3,8 @@ import type {
|
||||
BrowseFilesystemResponse,
|
||||
CloneProgressEvent,
|
||||
CreateFolderResponse,
|
||||
CreateWorkspacePayload,
|
||||
CreateWorkspaceResponse,
|
||||
CreateProjectPayload,
|
||||
CreateProjectResponse,
|
||||
CredentialsResponse,
|
||||
FolderSuggestion,
|
||||
TokenMode,
|
||||
@@ -27,6 +27,42 @@ const parseJson = async <T>(response: Response): Promise<T> => {
|
||||
return data;
|
||||
};
|
||||
|
||||
const resolveCreateProjectErrorMessage = (responseData: CreateProjectResponse): string | null => {
|
||||
if (typeof responseData.details === 'string' && responseData.details.trim().length > 0) {
|
||||
return responseData.details;
|
||||
}
|
||||
|
||||
if (typeof responseData.error === 'string' && responseData.error.trim().length > 0) {
|
||||
return responseData.error;
|
||||
}
|
||||
|
||||
if (responseData.error && typeof responseData.error === 'object') {
|
||||
const errorObject = responseData.error as { message?: unknown; details?: unknown };
|
||||
|
||||
if (typeof errorObject.details === 'string' && errorObject.details.trim().length > 0) {
|
||||
return errorObject.details;
|
||||
}
|
||||
|
||||
if (typeof errorObject.message === 'string' && errorObject.message.trim().length > 0) {
|
||||
return errorObject.message;
|
||||
}
|
||||
|
||||
if (
|
||||
errorObject.details
|
||||
&& typeof errorObject.details === 'object'
|
||||
&& typeof (errorObject.details as { projectPath?: unknown }).projectPath === 'string'
|
||||
) {
|
||||
return `Project path already exists: ${(errorObject.details as { projectPath: string }).projectPath}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof responseData.message === 'string' && responseData.message.trim().length > 0) {
|
||||
return responseData.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const fetchGithubTokenCredentials = async () => {
|
||||
const response = await api.get('/settings/credentials?type=github_token');
|
||||
const data = await parseJson<CredentialsResponse>(response);
|
||||
@@ -64,12 +100,12 @@ export const createFolderInFilesystem = async (folderPath: string) => {
|
||||
return data.path || folderPath;
|
||||
};
|
||||
|
||||
export const createWorkspaceRequest = async (payload: CreateWorkspacePayload) => {
|
||||
const response = await api.createWorkspace(payload);
|
||||
const data = await parseJson<CreateWorkspaceResponse>(response);
|
||||
export const createProjectRequest = async (payload: CreateProjectPayload) => {
|
||||
const response = await api.createProject(payload);
|
||||
const data = await parseJson<CreateProjectResponse>(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.details || data.error || 'Failed to create workspace');
|
||||
throw new Error(resolveCreateProjectErrorMessage(data) || 'Failed to create project');
|
||||
}
|
||||
|
||||
return data.project;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export type WizardStep = 1 | 2 | 3;
|
||||
|
||||
export type WorkspaceType = 'existing' | 'new';
|
||||
export type WizardStep = 1 | 2;
|
||||
|
||||
export type TokenMode = 'stored' | 'new' | 'none';
|
||||
|
||||
@@ -34,16 +32,23 @@ export type CreateFolderResponse = {
|
||||
details?: string;
|
||||
};
|
||||
|
||||
export type CreateWorkspacePayload = {
|
||||
workspaceType: WorkspaceType;
|
||||
export type CreateProjectPayload = {
|
||||
path: string;
|
||||
customName?: string;
|
||||
};
|
||||
|
||||
export type CreateWorkspaceResponse = {
|
||||
export type CreateProjectApiError = {
|
||||
code?: string;
|
||||
message?: string;
|
||||
details?: unknown;
|
||||
};
|
||||
|
||||
export type CreateProjectResponse = {
|
||||
success?: boolean;
|
||||
project?: Record<string, unknown>;
|
||||
error?: string;
|
||||
error?: string | CreateProjectApiError;
|
||||
details?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type CloneProgressEvent = {
|
||||
@@ -53,7 +58,6 @@ export type CloneProgressEvent = {
|
||||
};
|
||||
|
||||
export type WizardFormState = {
|
||||
workspaceType: WorkspaceType;
|
||||
workspacePath: string;
|
||||
githubUrl: string;
|
||||
tokenMode: TokenMode;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { WorkspaceType } from '../types';
|
||||
|
||||
const SSH_PREFIXES = ['git@', 'ssh://'];
|
||||
const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:\\?$/;
|
||||
|
||||
@@ -8,13 +6,11 @@ export const isSshGitUrl = (url: string): boolean => {
|
||||
return SSH_PREFIXES.some((prefix) => trimmedUrl.startsWith(prefix));
|
||||
};
|
||||
|
||||
export const shouldShowGithubAuthentication = (
|
||||
workspaceType: WorkspaceType,
|
||||
githubUrl: string,
|
||||
): boolean => workspaceType === 'new' && githubUrl.trim().length > 0 && !isSshGitUrl(githubUrl);
|
||||
export const shouldShowGithubAuthentication = (githubUrl: string): boolean =>
|
||||
githubUrl.trim().length > 0 && !isSshGitUrl(githubUrl);
|
||||
|
||||
export const isCloneWorkflow = (workspaceType: WorkspaceType, githubUrl: string): boolean =>
|
||||
workspaceType === 'new' && githubUrl.trim().length > 0;
|
||||
export const isCloneWorkflow = (githubUrl: string): boolean =>
|
||||
githubUrl.trim().length > 0;
|
||||
|
||||
export const getSuggestionRootPath = (inputPath: string): string => {
|
||||
const trimmedPath = inputPath.trim();
|
||||
|
||||
@@ -110,10 +110,10 @@ export const api = {
|
||||
if (token) params.set('token', token);
|
||||
return `/api/search/conversations?${params.toString()}`;
|
||||
},
|
||||
createWorkspace: (workspaceData) =>
|
||||
authenticatedFetch('/api/projects/create-workspace', {
|
||||
createProject: (projectData) =>
|
||||
authenticatedFetch('/api/projects/create-project', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(workspaceData),
|
||||
body: JSON.stringify(projectData),
|
||||
}),
|
||||
readFile: (projectId, filePath) =>
|
||||
authenticatedFetch(`/api/projects/${projectId}/file?filePath=${encodeURIComponent(filePath)}`),
|
||||
|
||||
Reference in New Issue
Block a user