Improve team admin screen workflow for team/members. Closes  #2767

This commit is contained in:
Francis Cao 2026-01-26 10:00:31 -08:00
parent 128217c0f4
commit 7f43a0d41a
9 changed files with 230 additions and 8 deletions

View file

@ -3,14 +3,19 @@ import { Column } from '@umami/react-zen';
import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel';
import { useMessages } from '@/components/hooks';
import { TeamsAddButton } from '../../teams/TeamsAddButton';
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
export function AdminTeamsPage() {
const { formatMessage, labels } = useMessages();
const handleSave = () => {};
return (
<Column gap="6" margin="2">
<PageHeader title={formatMessage(labels.teams)} />
<PageHeader title={formatMessage(labels.teams)}>
<TeamsAddButton onSave={handleSave} isAdmin={true} />
</PageHeader>
<Panel>
<AdminTeamsDataTable />
</Panel>

View file

@ -7,8 +7,17 @@ import {
TextField,
} from '@umami/react-zen';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import { UserSelect } from '@/components/input/UserSelect';
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
export function TeamAddForm({
onSave,
onClose,
isAdmin,
}: {
onSave: () => void;
onClose: () => void;
isAdmin: boolean;
}) {
const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
@ -26,6 +35,11 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
<FormField name="name" label={formatMessage(labels.name)}>
<TextField autoComplete="off" />
</FormField>
{isAdmin && (
<FormField name="ownerId" label={formatMessage(labels.teamOwner)}>
<UserSelect buttonProps={{ style: { outline: 'none' } }} />
</FormField>
)}
<FormButtons>
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}

View file

@ -0,0 +1,76 @@
import {
Button,
Form,
FormButtons,
FormField,
FormSubmitButton,
ListItem,
Select,
} from '@umami/react-zen';
import { useMessages, useUpdateQuery } from '@/components/hooks';
import { UserSelect } from '@/components/input/UserSelect';
import { ROLES } from '@/lib/constants';
const roles = [ROLES.teamManager, ROLES.teamMember, ROLES.teamViewOnly];
export function TeamMemberAddForm({
teamId,
onSave,
onClose,
}: {
teamId: string;
onSave?: () => void;
onClose?: () => void;
}) {
const { formatMessage, labels, getErrorMessage } = useMessages();
const { mutateAsync, error, isPending } = useUpdateQuery(`/teams/${teamId}/users`);
const handleSubmit = async (data: any) => {
await mutateAsync(data, {
onSuccess: async () => {
onSave?.();
onClose?.();
},
});
};
const renderRole = role => {
switch (role) {
case ROLES.teamManager:
return formatMessage(labels.manager);
case ROLES.teamMember:
return formatMessage(labels.member);
case ROLES.teamViewOnly:
return formatMessage(labels.viewOnly);
}
};
return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
<FormField
name="userId"
label={formatMessage(labels.username)}
rules={{ required: 'Required' }}
>
<UserSelect teamId={teamId} />
</FormField>
<FormField name="role" label={formatMessage(labels.role)} rules={{ required: 'Required' }}>
<Select items={roles} renderValue={value => renderRole(value as any)}>
{roles.map(value => (
<ListItem key={value} id={value}>
{renderRole(value)}
</ListItem>
))}
</Select>
</FormField>
<FormButtons>
<Button isDisabled={isPending} onPress={onClose}>
{formatMessage(labels.cancel)}
</Button>
<FormSubmitButton variant="primary" isDisabled={isPending}>
{formatMessage(labels.save)}
</FormSubmitButton>
</FormButtons>
</Form>
);
}

View file

@ -4,7 +4,13 @@ import { Plus } from '@/components/icons';
import { messages } from '@/components/messages';
import { TeamAddForm } from './TeamAddForm';
export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
export function TeamsAddButton({
onSave,
isAdmin = false,
}: {
onSave?: () => void;
isAdmin?: boolean;
}) {
const { formatMessage, labels } = useMessages();
const { toast } = useToast();
const { touch } = useModified();
@ -25,7 +31,7 @@ export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
</Button>
<Modal>
<Dialog title={formatMessage(labels.createTeam)} style={{ width: 400 }}>
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} />}
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} isAdmin={isAdmin} />}
</Dialog>
</Modal>
</DialogTrigger>

View file

@ -0,0 +1,40 @@
import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
import { useMessages, useModified } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { messages } from '@/components/messages';
import { TeamMemberAddForm } from './TeamMemberAddForm';
export function TeamsMemberAddButton({
teamId,
onSave,
}: {
teamId: string;
onSave?: () => void;
isAdmin?: boolean;
}) {
const { formatMessage, labels } = useMessages();
const { toast } = useToast();
const { touch } = useModified();
const handleSave = async () => {
toast(formatMessage(messages.saved));
touch('teams:members');
onSave?.();
};
return (
<DialogTrigger>
<Button>
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.addMember)}</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.addMember)} style={{ width: 400 }}>
{({ close }) => <TeamMemberAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
);
}

View file

@ -1,10 +1,12 @@
import { Column } from '@umami/react-zen';
import { Column, Heading, Row } from '@umami/react-zen';
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel';
import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks';
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
import { Users } from '@/components/icons';
import { labels } from '@/components/messages';
import { ROLES } from '@/lib/constants';
import { TeamsMemberAddButton } from '../TeamsMemberAddButton';
import { TeamEditForm } from './TeamEditForm';
import { TeamManage } from './TeamManage';
import { TeamMembersDataTable } from './TeamMembersDataTable';
@ -13,6 +15,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
const team: any = useTeam();
const { user } = useLoginQuery();
const { pathname } = useNavigation();
const { formatMessage } = useMessages();
const isAdmin = pathname.includes('/admin');
@ -37,6 +40,10 @@ export function TeamSettings({ teamId }: { teamId: string }) {
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
</Panel>
<Panel>
<Row alignItems="center" justifyContent="space-between">
<Heading size="2">{formatMessage(labels.members)}</Heading>
{isAdmin && <TeamsMemberAddButton teamId={teamId} />}
</Row>
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
</Panel>
{isTeamOwner && (