Add multi-tenancy with Teams feature
This commit is contained in:
251
frontend/src/pages/TeamSettingsPage.tsx
Normal file
251
frontend/src/pages/TeamSettingsPage.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user