* All type checks pass. Let me provide a summary of the changes made:
## Summary
I've added a "Repository settings" option to the Git panel dropdown in the Workspaces UI. Here are the changes made:
### 1. `frontend/src/components/ui-new/primitives/RepoCard.tsx`
- Added `DotsThreeIcon` and `GearIcon` imports from phosphor-icons
- Added `onOpenSettings?: () => void` prop to the `RepoCardProps` interface
- Changed the dropdown trigger icon from `ArrowSquareOutIcon` to `DotsThreeIcon` (three dots)
- Added a new dropdown menu item "Repository settings" with a gear icon that calls `onOpenSettings`
### 2. `frontend/src/components/ui-new/views/GitPanel.tsx`
- Added `onOpenSettings?: (repoId: string) => void` prop to the `GitPanelProps` interface
- Added `onOpenSettings` to the destructured props
- Passed `onOpenSettings` callback to each `RepoCard` component
### 3. `frontend/src/components/ui-new/containers/WorkspacesLayout.tsx`
- Added `useNavigate` import from react-router-dom
- Added `const navigate = useNavigate()` in `GitPanelContainer`
- Added `handleOpenSettings` callback that navigates to `/settings/repos?repoId=${repoId}`
- Passed `onOpenSettings={handleOpenSettings}` to the `GitPanel` component
### 4. `frontend/src/i18n/locales/en/common.json`
- Added `"repoSettings": "Repository settings"` translation under the `actions` section
* All checks pass:
- ✅ i18n check - Translation keys are consistent across all locales
- ✅ Linter - No lint errors
- ✅ TypeScript type check - No type errors
281 lines
9.3 KiB
TypeScript
281 lines
9.3 KiB
TypeScript
import {
|
|
GitBranchIcon,
|
|
GitPullRequestIcon,
|
|
ArrowsClockwiseIcon,
|
|
FileTextIcon,
|
|
ArrowUpIcon,
|
|
CrosshairIcon,
|
|
ArrowRightIcon,
|
|
CodeIcon,
|
|
ArrowSquareOutIcon,
|
|
CopyIcon,
|
|
GitMergeIcon,
|
|
CheckCircleIcon,
|
|
SpinnerGapIcon,
|
|
WarningCircleIcon,
|
|
DotsThreeIcon,
|
|
GearIcon,
|
|
} from '@phosphor-icons/react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuTriggerButton,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
} from './Dropdown';
|
|
import { CollapsibleSection } from './CollapsibleSection';
|
|
import { SplitButton, type SplitButtonOption } from './SplitButton';
|
|
import { useRepoAction, PERSIST_KEYS } from '@/stores/useUiPreferencesStore';
|
|
|
|
export type RepoAction =
|
|
| 'pull-request'
|
|
| 'merge'
|
|
| 'change-target'
|
|
| 'rebase'
|
|
| 'push';
|
|
|
|
const repoActionOptions: SplitButtonOption<RepoAction>[] = [
|
|
{
|
|
value: 'pull-request',
|
|
label: 'Open pull request',
|
|
icon: GitPullRequestIcon,
|
|
},
|
|
{ value: 'merge', label: 'Merge', icon: GitMergeIcon },
|
|
];
|
|
|
|
interface RepoCardProps {
|
|
repoId: string;
|
|
name: string;
|
|
targetBranch: string;
|
|
commitsAhead?: number;
|
|
filesChanged?: number;
|
|
linesAdded?: number;
|
|
linesRemoved?: number;
|
|
prNumber?: number;
|
|
prUrl?: string;
|
|
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
|
showPushButton?: boolean;
|
|
isPushPending?: boolean;
|
|
isPushSuccess?: boolean;
|
|
isPushError?: boolean;
|
|
branchDropdownContent?: React.ReactNode;
|
|
onChangeTarget?: () => void;
|
|
onRebase?: () => void;
|
|
onActionsClick?: (action: RepoAction) => void;
|
|
onPushClick?: () => void;
|
|
onOpenInEditor?: () => void;
|
|
onCopyPath?: () => void;
|
|
onOpenSettings?: () => void;
|
|
}
|
|
|
|
export function RepoCard({
|
|
repoId,
|
|
name,
|
|
targetBranch,
|
|
commitsAhead = 0,
|
|
filesChanged = 0,
|
|
linesAdded,
|
|
linesRemoved,
|
|
prNumber,
|
|
prUrl,
|
|
prStatus,
|
|
showPushButton = false,
|
|
isPushPending = false,
|
|
isPushSuccess = false,
|
|
isPushError = false,
|
|
branchDropdownContent,
|
|
onChangeTarget,
|
|
onRebase,
|
|
onActionsClick,
|
|
onPushClick,
|
|
onOpenInEditor,
|
|
onCopyPath,
|
|
onOpenSettings,
|
|
}: RepoCardProps) {
|
|
const { t } = useTranslation('tasks');
|
|
const { t: tCommon } = useTranslation('common');
|
|
const [selectedAction, setSelectedAction] = useRepoAction(repoId);
|
|
|
|
return (
|
|
<CollapsibleSection
|
|
persistKey={PERSIST_KEYS.repoCard(repoId)}
|
|
title={name}
|
|
className="gap-half"
|
|
defaultExpanded
|
|
>
|
|
{/* Branch row */}
|
|
<div className="flex items-center gap-base">
|
|
<div className="flex items-center justify-center">
|
|
<GitBranchIcon className="size-icon-base text-base" weight="fill" />
|
|
</div>
|
|
<div className="flex items-center justify-center">
|
|
<ArrowRightIcon className="size-icon-sm text-low" weight="bold" />
|
|
</div>
|
|
<div className="flex items-center justify-center">
|
|
<CrosshairIcon className="size-icon-sm text-low" weight="bold" />
|
|
</div>
|
|
<div className="flex-1 min-w-0 flex">
|
|
<DropdownMenu>
|
|
<DropdownMenuTriggerButton
|
|
label={targetBranch}
|
|
className="max-w-full"
|
|
/>
|
|
<DropdownMenuContent>
|
|
{branchDropdownContent ?? (
|
|
<>
|
|
<DropdownMenuItem
|
|
icon={CrosshairIcon}
|
|
onClick={onChangeTarget}
|
|
>
|
|
{t('git.actions.changeTarget')}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
icon={ArrowsClockwiseIcon}
|
|
onClick={onRebase}
|
|
>
|
|
{t('rebase.common.action')}
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
className="flex items-center justify-center p-1.5 rounded hover:bg-tertiary text-low hover:text-base transition-colors"
|
|
title="Repo actions"
|
|
>
|
|
<DotsThreeIcon className="size-icon-base" weight="bold" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem icon={CopyIcon} onClick={onCopyPath}>
|
|
{tCommon('actions.copyPath')}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem icon={CodeIcon} onClick={onOpenInEditor}>
|
|
{tCommon('actions.openInIde')}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem icon={GearIcon} onClick={onOpenSettings}>
|
|
{tCommon('actions.repoSettings')}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Commits badge */}
|
|
{commitsAhead > 0 && (
|
|
<div className="flex items-center py-half">
|
|
<span className="text-sm font-medium text-brand-secondary">
|
|
{commitsAhead}
|
|
</span>
|
|
<ArrowUpIcon
|
|
className="size-icon-xs text-brand-secondary"
|
|
weight="bold"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Files changed row */}
|
|
<div className="flex items-center justify-between w-full">
|
|
<div className="flex items-center gap-half">
|
|
<FileTextIcon className="size-icon-xs text-low" />
|
|
<span className="text-sm font-medium text-low truncate">
|
|
{t('diff.filesChanged', { count: filesChanged })}
|
|
</span>
|
|
</div>
|
|
<span className="text-sm font-semibold text-right">
|
|
{linesAdded !== undefined && (
|
|
<span className="text-success">+{linesAdded} </span>
|
|
)}
|
|
{linesRemoved !== undefined && (
|
|
<span className="text-error">-{linesRemoved}</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* PR status row */}
|
|
{prNumber && (
|
|
<div className="flex items-center gap-half my-base">
|
|
{prStatus === 'merged' ? (
|
|
prUrl ? (
|
|
<button
|
|
onClick={() => window.open(prUrl, '_blank')}
|
|
className="inline-flex items-center gap-half px-base py-half rounded-sm bg-panel text-success hover:bg-tertiary text-sm font-medium transition-colors"
|
|
>
|
|
<CheckCircleIcon className="size-icon-xs" weight="fill" />
|
|
{t('git.pr.merged', { prNumber })}
|
|
<ArrowSquareOutIcon className="size-icon-xs" weight="bold" />
|
|
</button>
|
|
) : (
|
|
<span className="inline-flex items-center gap-half px-base py-half rounded-sm bg-panel text-success text-sm font-medium">
|
|
<CheckCircleIcon className="size-icon-xs" weight="fill" />
|
|
{t('git.pr.merged', { prNumber })}
|
|
</span>
|
|
)
|
|
) : prUrl ? (
|
|
<button
|
|
onClick={() => window.open(prUrl, '_blank')}
|
|
className="inline-flex items-center gap-half px-base py-half rounded-sm bg-panel text-normal hover:bg-tertiary text-sm font-medium transition-colors"
|
|
>
|
|
<GitPullRequestIcon className="size-icon-xs" weight="fill" />
|
|
{t('git.pr.open', { number: prNumber })}
|
|
<ArrowSquareOutIcon className="size-icon-xs" weight="bold" />
|
|
</button>
|
|
) : (
|
|
<span className="inline-flex items-center gap-half px-base py-half rounded-sm bg-panel text-normal text-sm font-medium">
|
|
<GitPullRequestIcon className="size-icon-xs" weight="fill" />
|
|
{t('git.pr.open', { number: prNumber })}
|
|
</span>
|
|
)}
|
|
{/* Push button - shows loading/success/error state */}
|
|
{(showPushButton ||
|
|
isPushPending ||
|
|
isPushSuccess ||
|
|
isPushError) && (
|
|
<button
|
|
onClick={onPushClick}
|
|
disabled={isPushPending || isPushSuccess || isPushError}
|
|
className={`inline-flex items-center gap-half px-base py-half rounded-sm text-sm font-medium transition-colors disabled:cursor-not-allowed ${
|
|
isPushSuccess
|
|
? 'bg-success/20 text-success'
|
|
: isPushError
|
|
? 'bg-error/20 text-error'
|
|
: 'bg-panel text-normal hover:bg-tertiary disabled:opacity-50'
|
|
}`}
|
|
>
|
|
{isPushPending ? (
|
|
<SpinnerGapIcon className="size-icon-xs animate-spin" />
|
|
) : isPushSuccess ? (
|
|
<CheckCircleIcon className="size-icon-xs" weight="fill" />
|
|
) : isPushError ? (
|
|
<WarningCircleIcon className="size-icon-xs" weight="fill" />
|
|
) : (
|
|
<ArrowUpIcon className="size-icon-xs" weight="bold" />
|
|
)}
|
|
{isPushPending
|
|
? t('git.states.pushing')
|
|
: isPushSuccess
|
|
? t('git.states.pushed')
|
|
: isPushError
|
|
? t('git.states.pushFailed')
|
|
: t('git.states.push')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Actions row */}
|
|
<div className="my-base">
|
|
<SplitButton
|
|
options={repoActionOptions}
|
|
selectedValue={selectedAction}
|
|
onSelectionChange={setSelectedAction}
|
|
onAction={(action) => onActionsClick?.(action)}
|
|
/>
|
|
</div>
|
|
</CollapsibleSection>
|
|
);
|
|
}
|