177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
|
|
import { api } from '@renderer/api';
|
|
import { Button } from '@renderer/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@renderer/components/ui/dialog';
|
|
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
|
import { cn } from '@renderer/lib/utils';
|
|
import { Loader2 } from 'lucide-react';
|
|
|
|
const TEAM_COLOR_NAMES = [
|
|
'blue',
|
|
'green',
|
|
'red',
|
|
'yellow',
|
|
'purple',
|
|
'cyan',
|
|
'orange',
|
|
'pink',
|
|
] as const;
|
|
|
|
interface EditTeamDialogProps {
|
|
open: boolean;
|
|
teamName: string;
|
|
currentName: string;
|
|
currentDescription: string;
|
|
currentColor: string;
|
|
onClose: () => void;
|
|
onSaved: () => void;
|
|
}
|
|
|
|
export const EditTeamDialog = ({
|
|
open,
|
|
teamName,
|
|
currentName,
|
|
currentDescription,
|
|
currentColor,
|
|
onClose,
|
|
onSaved,
|
|
}: EditTeamDialogProps): React.JSX.Element => {
|
|
const [name, setName] = useState(currentName);
|
|
const [description, setDescription] = useState(currentDescription);
|
|
const [color, setColor] = useState(currentColor);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setName(currentName);
|
|
setDescription(currentDescription);
|
|
setColor(currentColor);
|
|
setError(null);
|
|
}
|
|
}, [open, currentName, currentDescription, currentColor]);
|
|
|
|
const handleSave = (): void => {
|
|
if (!name.trim()) {
|
|
setError('Team name cannot be empty');
|
|
return;
|
|
}
|
|
setSaving(true);
|
|
setError(null);
|
|
void (async () => {
|
|
try {
|
|
await api.teams.updateConfig(teamName, {
|
|
name: name.trim(),
|
|
description: description.trim(),
|
|
color,
|
|
});
|
|
onSaved();
|
|
onClose();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Failed to save');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
})();
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(nextOpen) => !nextOpen && onClose()}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Team</DialogTitle>
|
|
<DialogDescription>Change team name, description and color</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label
|
|
htmlFor="edit-team-name"
|
|
className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]"
|
|
>
|
|
Name
|
|
</label>
|
|
<input
|
|
id="edit-team-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' && !saving && name.trim()) handleSave();
|
|
}}
|
|
className="w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-sm text-[var(--color-text)] outline-none focus:border-[var(--color-border-emphasis)]"
|
|
placeholder="Team name"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
htmlFor="edit-team-description"
|
|
className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]"
|
|
>
|
|
Description
|
|
</label>
|
|
<textarea
|
|
id="edit-team-description"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
rows={3}
|
|
className="w-full resize-none rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-sm text-[var(--color-text)] outline-none focus:border-[var(--color-border-emphasis)]"
|
|
placeholder="Team description (optional)"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-[var(--color-text-secondary)]">
|
|
Color (optional)
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{TEAM_COLOR_NAMES.map((colorName) => {
|
|
const colorSet = getTeamColorSet(colorName);
|
|
const isSelected = color === colorName;
|
|
return (
|
|
<button
|
|
key={colorName}
|
|
type="button"
|
|
className={cn(
|
|
'flex size-7 items-center justify-center rounded-full border-2 transition-all',
|
|
isSelected ? 'scale-110' : 'opacity-70 hover:opacity-100'
|
|
)}
|
|
style={{
|
|
backgroundColor: colorSet.badge,
|
|
borderColor: isSelected ? colorSet.border : 'transparent',
|
|
}}
|
|
title={colorName}
|
|
onClick={() => setColor(isSelected ? '' : colorName)}
|
|
>
|
|
<span
|
|
className="size-3.5 rounded-full"
|
|
style={{ backgroundColor: colorSet.border }}
|
|
/>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
{error && <p className="text-xs text-red-400">{error}</p>}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" size="sm" onClick={onClose} disabled={saving}>
|
|
Cancel
|
|
</Button>
|
|
<Button size="sm" onClick={handleSave} disabled={saving || !name.trim()}>
|
|
{saving && <Loader2 size={14} className="mr-1.5 animate-spin" />}
|
|
Save
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|