feat(FRE-596): wire TeamManagement UI to real tRPC API
- Replace mock data with tRPC API calls for team CRUD operations - Add TeamList view with fetch, create, and delete teams - Add TeamDetail view with member management (list, invite, update role, remove, leave) - Use solid-js <For> for proper keyed list rendering - Add loading/error states and confirmation dialogs - Use @tanstack/react-query for data fetching and cache invalidation
This commit is contained in:
@@ -1,23 +1,82 @@
|
|||||||
import { Component, createSignal } from 'solid-js';
|
import { Component, createSignal, For, Show } from 'solid-js';
|
||||||
import { A } from '@solidjs/router';
|
import { A, useLocation } from '@solidjs/router';
|
||||||
import { useAuth } from '../../lib/auth';
|
import { createTRPCClient } from '../../trpc/client';
|
||||||
import { Team } from '../../lib/auth/types';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import type { Team as DbTeam, TeamMember as DbTeamMember } from '../../db/schema/teams';
|
||||||
|
|
||||||
export const TeamManagement: Component<any> = () => {
|
const API_URL = (import.meta as any).env?.VITE_API_URL || 'http://localhost:8080';
|
||||||
const auth = useAuth();
|
|
||||||
const [teams] = createSignal<Team[]>([
|
function formatDateTime(iso: Date | string): string {
|
||||||
{
|
return new Date(iso).toLocaleDateString('en-US', {
|
||||||
id: 'team_default',
|
month: 'short',
|
||||||
name: 'My Workspace',
|
day: 'numeric',
|
||||||
members: [
|
year: 'numeric',
|
||||||
{ userId: auth().user?.id || '', role: 'owner', joinedAt: new Date().toISOString() },
|
});
|
||||||
],
|
}
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
const TeamList: Component = () => {
|
||||||
},
|
const queryClient = useQueryClient();
|
||||||
]);
|
const client = createTRPCClient(API_URL);
|
||||||
const [showCreateDialog, setShowCreateDialog] = createSignal(false);
|
const [showCreateDialog, setShowCreateDialog] = createSignal(false);
|
||||||
const [newTeamName, setNewTeamName] = createSignal('');
|
const [newTeamName, setNewTeamName] = createSignal('');
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
|
||||||
|
const teamsQuery = useQuery({
|
||||||
|
queryKey: ['team', 'listTeams'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.listTeams`, {
|
||||||
|
headers: auth ? { Authorization: `Bearer ${auth}` } : {},
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch teams');
|
||||||
|
const data = await res.json();
|
||||||
|
return data.data as DbTeam[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createTeamMutation = useMutation({
|
||||||
|
mutationFn: async (name: string) => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.createTeam`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ input: { name } }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(err.message || 'Failed to create team');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['team', 'listTeams'] });
|
||||||
|
setShowCreateDialog(false);
|
||||||
|
setNewTeamName('');
|
||||||
|
},
|
||||||
|
onError: (err) => setError(err.message),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteTeamMutation = useMutation({
|
||||||
|
mutationFn: async (id: string) => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.deleteTeam`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ input: { id } }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to delete team');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['team', 'listTeams'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="freno-teams">
|
<div class="freno-teams">
|
||||||
@@ -25,29 +84,70 @@ export const TeamManagement: Component<any> = () => {
|
|||||||
<h1>Teams</h1>
|
<h1>Teams</h1>
|
||||||
<button
|
<button
|
||||||
class="freno-btn freno-btn-primary"
|
class="freno-btn freno-btn-primary"
|
||||||
onClick={() => setShowCreateDialog(true)}
|
onClick={() => { setShowCreateDialog(true); setError(''); }}
|
||||||
>
|
>
|
||||||
+ New Team
|
+ New Team
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="freno-team-grid">
|
{error() && (
|
||||||
{teams().map((team) => (
|
<div class="freno-alert freno-alert-error">
|
||||||
<A href={`/teams/${team.id}`} class="freno-team-card">
|
{error()}
|
||||||
<div class="freno-team-icon">👥</div>
|
<button onClick={() => setError('')} class="freno-alert-dismiss">×</button>
|
||||||
<h3>{team.name}</h3>
|
</div>
|
||||||
<p class="freno-team-members">{team.members.length} member{team.members.length !== 1 ? 's' : ''}</p>
|
)}
|
||||||
<span class="freno-date">
|
|
||||||
Created {new Date(team.createdAt).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</A>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button class="freno-team-card freno-team-card-new" onClick={() => setShowCreateDialog(true)}>
|
{teamsQuery.isLoading ? (
|
||||||
<div class="freno-team-icon">+</div>
|
<div class="freno-loading">Loading teams...</div>
|
||||||
<h3>Create Team</h3>
|
) : teamsQuery.error ? (
|
||||||
</button>
|
<div class="freno-empty-state">
|
||||||
</div>
|
<div class="freno-empty-icon">⚠️</div>
|
||||||
|
<h3>Failed to load teams</h3>
|
||||||
|
<p>{teamsQuery.error.message}</p>
|
||||||
|
<button class="freno-btn freno-btn-secondary" onClick={() => teamsQuery.refetch()}>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="freno-team-grid">
|
||||||
|
<For each={teamsQuery.data || []}>
|
||||||
|
{(team) => (
|
||||||
|
<div class="freno-team-card">
|
||||||
|
<A href={`/teams/${team.id}`} class="freno-team-card-link">
|
||||||
|
<div class="freno-team-icon">👥</div>
|
||||||
|
<h3>{team.name}</h3>
|
||||||
|
<span class="freno-date">
|
||||||
|
Created {formatDateTime(team.createdAt)}
|
||||||
|
</span>
|
||||||
|
</A>
|
||||||
|
<div class="freno-team-card-actions">
|
||||||
|
<button
|
||||||
|
class="freno-btn freno-btn-danger-sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm(`Delete team "${team.name}"? This cannot be undone.`)) {
|
||||||
|
deleteTeamMutation.mutate(team.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={deleteTeamMutation.isPending}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="freno-team-card freno-team-card-new"
|
||||||
|
onClick={() => { setShowCreateDialog(true); setError(''); }}
|
||||||
|
>
|
||||||
|
<div class="freno-team-icon">+</div>
|
||||||
|
<h3>Create Team</h3>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showCreateDialog() && (
|
{showCreateDialog() && (
|
||||||
<div class="freno-modal-overlay" onClick={() => setShowCreateDialog(false)}>
|
<div class="freno-modal-overlay" onClick={() => setShowCreateDialog(false)}>
|
||||||
@@ -58,9 +158,10 @@ export const TeamManagement: Component<any> = () => {
|
|||||||
</div>
|
</div>
|
||||||
<form class="freno-form" onSubmit={(e) => {
|
<form class="freno-form" onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (newTeamName().trim()) {
|
const name = newTeamName().trim();
|
||||||
setShowCreateDialog(false);
|
if (name) {
|
||||||
setNewTeamName('');
|
setError('');
|
||||||
|
createTeamMutation.mutate(name);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<div class="freno-form-group">
|
<div class="freno-form-group">
|
||||||
@@ -73,11 +174,22 @@ export const TeamManagement: Component<any> = () => {
|
|||||||
value={newTeamName()}
|
value={newTeamName()}
|
||||||
onInput={(e) => setNewTeamName(e.target.value)}
|
onInput={(e) => setNewTeamName(e.target.value)}
|
||||||
autofocus
|
autofocus
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="freno-form-actions">
|
<div class="freno-form-actions">
|
||||||
<button type="submit" class="freno-btn freno-btn-primary">Create Team</button>
|
<button
|
||||||
<button type="button" class="freno-btn freno-btn-secondary" onClick={() => setShowCreateDialog(false)}>
|
type="submit"
|
||||||
|
class="freno-btn freno-btn-primary"
|
||||||
|
disabled={createTeamMutation.isPending || !newTeamName().trim()}
|
||||||
|
>
|
||||||
|
{createTeamMutation.isPending ? 'Creating...' : 'Create Team'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="freno-btn freno-btn-secondary"
|
||||||
|
onClick={() => setShowCreateDialog(false)}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,3 +200,305 @@ export const TeamManagement: Component<any> = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TeamDetail: Component<{ teamId: string }> = ({ teamId }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
const [showInviteDialog, setShowInviteDialog] = createSignal(false);
|
||||||
|
const [inviteUserId, setInviteUserId] = createSignal('');
|
||||||
|
const [inviteRole, setInviteRole] = createSignal<'editor' | 'viewer' | 'admin'>('editor');
|
||||||
|
|
||||||
|
const teamQuery = useQuery({
|
||||||
|
queryKey: ['team', 'getTeam', teamId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.getTeam?input=${encodeURIComponent(JSON.stringify({ id: teamId }))}`, {
|
||||||
|
headers: auth ? { Authorization: `Bearer ${auth}` } : {},
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch team');
|
||||||
|
const data = await res.json();
|
||||||
|
return data.data as DbTeam;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const membersQuery = useQuery({
|
||||||
|
queryKey: ['team', 'listMembers', teamId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.listMembers?input=${encodeURIComponent(JSON.stringify({ teamId }))}`, {
|
||||||
|
headers: auth ? { Authorization: `Bearer ${auth}` } : {},
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to fetch members');
|
||||||
|
const data = await res.json();
|
||||||
|
return data.data as DbTeamMember[];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRoleMutation = useMutation({
|
||||||
|
mutationFn: async ({ userId, role }: { userId: number; role: string }) => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.updateMemberRole`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ input: { teamId, userId, role } }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to update role');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['team', 'listMembers', teamId] });
|
||||||
|
},
|
||||||
|
onError: (err) => setError(err.message),
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeMemberMutation = useMutation({
|
||||||
|
mutationFn: async (userId: number) => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.removeMember`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ input: { teamId, userId } }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to remove member');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['team', 'listMembers', teamId] });
|
||||||
|
},
|
||||||
|
onError: (err) => setError(err.message),
|
||||||
|
});
|
||||||
|
|
||||||
|
const leaveTeamMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.leaveTeam`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ input: { teamId } }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to leave team');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
window.location.href = '/teams';
|
||||||
|
},
|
||||||
|
onError: (err) => setError(err.message),
|
||||||
|
});
|
||||||
|
|
||||||
|
const addMemberMutation = useMutation({
|
||||||
|
mutationFn: async ({ userId, role }: { userId: number; role: string }) => {
|
||||||
|
const auth = localStorage.getItem('clerk_token');
|
||||||
|
const res = await fetch(`${API_URL}/trpc/team.addMember`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(auth ? { Authorization: `Bearer ${auth}` } : {}),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ input: { teamId, userId, role } }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(err.message || 'Failed to add member');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['team', 'listMembers', teamId] });
|
||||||
|
setShowInviteDialog(false);
|
||||||
|
setInviteUserId('');
|
||||||
|
},
|
||||||
|
onError: (err) => setError(err.message),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="freno-team-detail">
|
||||||
|
<div class="freno-page-header">
|
||||||
|
<div>
|
||||||
|
<A href="/teams" class="freno-back-link">← Back to Teams</A>
|
||||||
|
<h1>
|
||||||
|
{teamQuery.isLoading ? 'Loading...' : teamQuery.data?.name || 'Team'}
|
||||||
|
</h1>
|
||||||
|
<p class="freno-team-meta">
|
||||||
|
Created {teamQuery.data ? formatDateTime(teamQuery.data.createdAt) : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="freno-header-actions">
|
||||||
|
<button
|
||||||
|
class="freno-btn freno-btn-primary"
|
||||||
|
onClick={() => setShowInviteDialog(true)}
|
||||||
|
>
|
||||||
|
+ Invite Member
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="freno-btn freno-btn-danger"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm('Leave this team? You will lose access to shared projects.')) {
|
||||||
|
leaveTeamMutation.mutate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={leaveTeamMutation.isPending}
|
||||||
|
>
|
||||||
|
Leave Team
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error() && (
|
||||||
|
<div class="freno-alert freno-alert-error">
|
||||||
|
{error()}
|
||||||
|
<button onClick={() => setError('')} class="freno-alert-dismiss">×</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="freno-team-members-section">
|
||||||
|
<h2>Members ({membersQuery.data?.length || 0})</h2>
|
||||||
|
|
||||||
|
{membersQuery.isLoading ? (
|
||||||
|
<div class="freno-loading">Loading members...</div>
|
||||||
|
) : membersQuery.error ? (
|
||||||
|
<div class="freno-empty-state">
|
||||||
|
<p>Failed to load members: {membersQuery.error.message}</p>
|
||||||
|
<button class="freno-btn freno-btn-secondary" onClick={() => membersQuery.refetch()}>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (membersQuery.data || []).length === 0 ? (
|
||||||
|
<div class="freno-empty-state">
|
||||||
|
<div class="freno-empty-icon">👤</div>
|
||||||
|
<h3>No members yet</h3>
|
||||||
|
<p>Invite people to collaborate with your team.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="freno-members-list">
|
||||||
|
<For each={membersQuery.data || []}>
|
||||||
|
{(member) => (
|
||||||
|
<div class="freno-member-row">
|
||||||
|
<div class="freno-member-info">
|
||||||
|
<span class="freno-member-id">User #{member.userId}</span>
|
||||||
|
<span class="freno-member-joined">Joined {formatDateTime(member.joinedAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="freno-member-actions">
|
||||||
|
<select
|
||||||
|
class="freno-select"
|
||||||
|
value={member.role}
|
||||||
|
onChange={(e) => {
|
||||||
|
setError('');
|
||||||
|
updateRoleMutation.mutate({ userId: member.userId, role: e.target.value });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="owner">Owner</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
<option value="editor">Editor</option>
|
||||||
|
<option value="viewer">Viewer</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="freno-btn freno-btn-danger-sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Remove this member from the team?`)) {
|
||||||
|
removeMemberMutation.mutate(member.userId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={member.role === 'owner'}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showInviteDialog() && (
|
||||||
|
<div class="freno-modal-overlay" onClick={() => setShowInviteDialog(false)}>
|
||||||
|
<div class="freno-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div class="freno-modal-header">
|
||||||
|
<h2>Invite Member</h2>
|
||||||
|
<button class="freno-btn-icon" onClick={() => setShowInviteDialog(false)}>✕</button>
|
||||||
|
</div>
|
||||||
|
<form class="freno-form" onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
const userId = parseInt(inviteUserId(), 10);
|
||||||
|
if (isNaN(userId) || userId <= 0) {
|
||||||
|
setError('Please enter a valid user ID.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addMemberMutation.mutate({ userId, role: inviteRole() });
|
||||||
|
}}>
|
||||||
|
<div class="freno-form-group">
|
||||||
|
<label class="freno-label" for="invite-user-id">User ID</label>
|
||||||
|
<input
|
||||||
|
class="freno-input"
|
||||||
|
id="invite-user-id"
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter user ID"
|
||||||
|
value={inviteUserId()}
|
||||||
|
onInput={(e) => setInviteUserId(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="freno-form-group">
|
||||||
|
<label class="freno-label" for="invite-role">Role</label>
|
||||||
|
<select
|
||||||
|
class="freno-select"
|
||||||
|
id="invite-role"
|
||||||
|
value={inviteRole()}
|
||||||
|
onChange={(e) => setInviteRole(e.target.value as 'editor' | 'viewer' | 'admin')}
|
||||||
|
>
|
||||||
|
<option value="editor">Editor</option>
|
||||||
|
<option value="viewer">Viewer</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="freno-form-actions">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="freno-btn freno-btn-primary"
|
||||||
|
disabled={addMemberMutation.isPending || !inviteUserId()}
|
||||||
|
>
|
||||||
|
{addMemberMutation.isPending ? 'Inviting...' : 'Invite'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="freno-btn freno-btn-secondary"
|
||||||
|
onClick={() => setShowInviteDialog(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TeamManagement: Component<any> = (props) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const teamId = () => {
|
||||||
|
const match = location.pathname.match(/^\/teams\/([^/]+)$/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={!teamId()} fallback={<TeamDetail teamId={teamId()!} />}>
|
||||||
|
<TeamList />
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user