252 lines
7.3 KiB
TypeScript
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">×</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;
|