Files
orchard/frontend/src/pages/TeamSettingsPage.tsx
2026-01-28 12:50:58 -06:00

252 lines
7.3 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { TeamDetail, TeamUpdate } from '../types';
import { getTeam, updateTeam, deleteTeam } from '../api';
import { useAuth } from '../contexts/AuthContext';
import { Breadcrumb } from '../components/Breadcrumb';
import './TeamSettingsPage.css';
function TeamSettingsPage() {
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const { user } = useAuth();
const [team, setTeam] = useState<TeamDetail | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [deleting, setDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const [formData, setFormData] = useState<TeamUpdate>({
name: '',
description: '',
});
const loadTeam = useCallback(async () => {
if (!slug) return;
try {
setLoading(true);
const teamData = await getTeam(slug);
setTeam(teamData);
setFormData({
name: teamData.name,
description: teamData.description || '',
});
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load team');
} finally {
setLoading(false);
}
}, [slug]);
useEffect(() => {
loadTeam();
}, [loadTeam]);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!slug || !team) return;
try {
setSaving(true);
setError(null);
const updatedTeam = await updateTeam(slug, formData);
setTeam(updatedTeam);
setSuccessMessage('Settings saved successfully');
setTimeout(() => setSuccessMessage(null), 3000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save settings');
} finally {
setSaving(false);
}
}
async function handleDelete() {
if (!slug || !team) return;
if (deleteConfirmText !== team.slug) return;
try {
setDeleting(true);
await deleteTeam(slug);
navigate('/teams');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete team');
setShowDeleteConfirm(false);
} finally {
setDeleting(false);
}
}
if (loading) {
return (
<div className="team-settings">
<div className="loading-state">Loading team settings...</div>
</div>
);
}
if (error && !team) {
return (
<div className="team-settings">
<div className="error-state">
<h2>Error loading team</h2>
<p>{error}</p>
<Link to="/teams" className="btn btn-primary">Back to Teams</Link>
</div>
</div>
);
}
if (!team) return null;
const isOwner = team.user_role === 'owner' || user?.is_admin;
const isAdmin = team.user_role === 'admin' || isOwner;
if (!isAdmin) {
return (
<div className="team-settings">
<div className="error-state">
<h2>Access Denied</h2>
<p>You need admin privileges to access team settings.</p>
<Link to={`/teams/${slug}`} className="btn btn-primary">Back to Team</Link>
</div>
</div>
);
}
return (
<div className="team-settings">
<Breadcrumb
items={[
{ label: 'Teams', href: '/teams' },
{ label: team.name, href: `/teams/${slug}` },
{ label: 'Settings' },
]}
/>
<h1>Team Settings</h1>
{error && (
<div className="error-message">
{error}
<button onClick={() => setError(null)} className="error-dismiss">&times;</button>
</div>
)}
{successMessage && (
<div className="success-message">
{successMessage}
</div>
)}
<form onSubmit={handleSubmit} className="settings-form">
<div className="form-section">
<h2>General</h2>
<div className="form-group">
<label htmlFor="team-name">Team Name</label>
<input
id="team-name"
type="text"
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
required
/>
</div>
<div className="form-group">
<label htmlFor="team-slug">Slug</label>
<input
id="team-slug"
type="text"
value={team.slug}
disabled
className="input-disabled"
/>
<span className="form-hint">Team slug cannot be changed</span>
</div>
<div className="form-group">
<label htmlFor="team-description">Description</label>
<textarea
id="team-description"
value={formData.description}
onChange={e => setFormData({ ...formData, description: e.target.value })}
rows={3}
placeholder="What is this team for?"
/>
</div>
<button type="submit" className="btn btn-primary" disabled={saving}>
{saving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
{isOwner && (
<div className="form-section danger-zone">
<h2>Danger Zone</h2>
<p className="danger-warning">
Deleting a team is permanent and cannot be undone.
You must move or delete all projects in this team first.
</p>
<button
type="button"
className="btn btn-danger"
onClick={() => setShowDeleteConfirm(true)}
>
Delete Team
</button>
</div>
)}
{showDeleteConfirm && (
<div className="modal-overlay" onClick={() => setShowDeleteConfirm(false)}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<h2>Delete Team</h2>
<p>
This will permanently delete the team <strong>{team.name}</strong>.
This action cannot be undone.
</p>
<p>
To confirm, type <strong>{team.slug}</strong> below:
</p>
<input
type="text"
value={deleteConfirmText}
onChange={e => setDeleteConfirmText(e.target.value)}
placeholder={team.slug}
className="delete-confirm-input"
/>
<div className="form-actions">
<button
type="button"
className="btn btn-secondary"
onClick={() => {
setShowDeleteConfirm(false);
setDeleteConfirmText('');
}}
>
Cancel
</button>
<button
type="button"
className="btn btn-danger"
disabled={deleteConfirmText !== team.slug || deleting}
onClick={handleDelete}
>
{deleting ? 'Deleting...' : 'Delete Team'}
</button>
</div>
</div>
</div>
)}
</div>
);
}
export default TeamSettingsPage;