import { useEffect, useMemo, useRef, useState } from 'react'; import type { Dispatch, SetStateAction } from 'react'; import type { ProjectLocation } from '@open-design/contracts'; import type { AppConfig } from '../types'; import { fetchProjectLocations, openProjectLocationFolderDialog, scanProjectLocations, updateProjectLocations, } from '../state/project-locations'; import { useI18n } from '../i18n'; import { Icon } from './Icon'; interface Props { cfg: AppConfig; setCfg: Dispatch>; onProjectsRefresh?: () => Promise | void; } interface DraftLocation { id?: string; path: string; } function locationLabel(locationPath: string): string { return locationPath.split(/[\\/]/).filter(Boolean).pop() || locationPath; } function externalLocations(locations: ProjectLocation[]): DraftLocation[] { return locations .filter((location) => !location.builtIn) .map((location) => ({ id: location.id, path: location.path })); } function toConfigLocations(locations: ProjectLocation[]): NonNullable { return locations .filter((location) => !location.builtIn) .map((location) => ({ id: location.id, name: location.name, path: location.path })); } export function ProjectLocationsSection({ cfg, setCfg, onProjectsRefresh }: Props) { const { t } = useI18n(); const [locations, setLocations] = useState([]); const [drafts, setDrafts] = useState(cfg.projectLocations ?? []); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [status, setStatus] = useState(null); const [error, setError] = useState(null); const draftsRef = useRef(drafts); useEffect(() => { draftsRef.current = drafts; }, [drafts]); useEffect(() => { let cancelled = false; setLoading(true); fetchProjectLocations() .then((next) => { if (cancelled) return; setLocations(next); setDrafts(externalLocations(next)); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [setCfg]); const builtIn = useMemo( () => locations.find((location) => location.builtIn), [locations], ); const effectiveDefaultLocationId = useMemo(() => { const configured = cfg.defaultProjectLocationId ?? 'default'; return locations.some((location) => location.id === configured) ? configured : 'default'; }, [cfg.defaultProjectLocationId, locations]); function defaultControlLabel(locationId: string): string { return effectiveDefaultLocationId === locationId ? t('settings.projectLocationsDefaultBadge') : t('settings.projectLocationsMakeDefault'); } function handleDefaultLocationChange(locationId: string) { setError(null); setStatus(t('settings.projectLocationsDefaultSaved')); setCfg((current) => ({ ...current, defaultProjectLocationId: locationId })); } async function save(nextDrafts: DraftLocation[]) { setSaving(true); setError(null); setStatus(null); try { const saved = await updateProjectLocations( nextDrafts.filter((location) => location.path.trim()), ); if (!saved) { setError(t('settings.projectLocationsSaveError')); return null; } setLocations(saved); const external = externalLocations(saved); setDrafts(external); setCfg((current) => { const configuredDefault = current.defaultProjectLocationId ?? 'default'; const nextDefault = saved.some((location) => location.id === configuredDefault) ? configuredDefault : 'default'; return { ...current, projectLocations: toConfigLocations(saved), defaultProjectLocationId: nextDefault, }; }); setStatus(t('settings.projectLocationsSaved')); void onProjectsRefresh?.(); return external; } finally { setSaving(false); } } async function runScan() { const result = await scanProjectLocations(); if (!result) { setError(t('settings.projectLocationsScanError')); return null; } setStatus(t('settings.projectLocationsScanComplete', { imported: result.imported.length, existing: result.existing.length, })); void onProjectsRefresh?.(); return result; } async function handleAddFolder() { setError(null); setStatus(null); const selected = await openProjectLocationFolderDialog(); if (!selected) { setStatus(t('settings.projectLocationsNoFolderSelected')); return; } if (draftsRef.current.some((draft) => draft.path === selected)) { setStatus(t('settings.projectLocationsDuplicate')); return; } const previous = draftsRef.current; const next = [...previous, { path: selected }]; setDrafts(next); const saved = await save(next); if (!saved) setDrafts(previous); else await runScan(); } async function removeDraft(index: number) { const previous = draftsRef.current; const next = previous.filter((_, i) => i !== index); setDrafts(next); const saved = await save(next); if (!saved) setDrafts(previous); } return (

{t('settings.projectLocations')}

{t('settings.projectLocationsDescription')}

{builtIn ? (
{t('newproj.locationDefault')} {builtIn.path}
) : null}
{drafts.map((draft, index) => (
{locationLabel(draft.path)} {draft.path} {t('settings.projectLocationsWorkBaseMeta')}
{draft.id ? ( ) : null}
))}
{status ?

{status}

: null} {error ?

{error}

: null}
); }