From 7f43a0d41a1b1418fcde01fa7b2f69f504f917f2 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Mon, 26 Jan 2026 10:00:31 -0800 Subject: [PATCH] =?UTF-8?q?Improve=20team=20admin=20screen=20workflow=20fo?= =?UTF-8?q?r=20team/members.=20Closes=20=C2=A0#2767?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/admin/teams/AdminTeamsPage.tsx | 7 +- src/app/(main)/teams/TeamAddForm.tsx | 16 +++- src/app/(main)/teams/TeamMemberAddForm.tsx | 76 +++++++++++++++++++ src/app/(main)/teams/TeamsAddButton.tsx | 10 ++- src/app/(main)/teams/TeamsMemberAddButton.tsx | 40 ++++++++++ .../(main)/teams/[teamId]/TeamSettings.tsx | 11 ++- src/app/api/teams/route.ts | 5 +- src/components/input/UserSelect.tsx | 71 +++++++++++++++++ src/index.ts | 2 + 9 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 src/app/(main)/teams/TeamMemberAddForm.tsx create mode 100644 src/app/(main)/teams/TeamsMemberAddButton.tsx create mode 100644 src/components/input/UserSelect.tsx diff --git a/src/app/(main)/admin/teams/AdminTeamsPage.tsx b/src/app/(main)/admin/teams/AdminTeamsPage.tsx index 41e6f4af..7905f7f6 100644 --- a/src/app/(main)/admin/teams/AdminTeamsPage.tsx +++ b/src/app/(main)/admin/teams/AdminTeamsPage.tsx @@ -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 ( - + + + diff --git a/src/app/(main)/teams/TeamAddForm.tsx b/src/app/(main)/teams/TeamAddForm.tsx index c95259f4..3b827776 100644 --- a/src/app/(main)/teams/TeamAddForm.tsx +++ b/src/app/(main)/teams/TeamAddForm.tsx @@ -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: + {isAdmin && ( + + + + )} + + {formatMessage(labels.save)} + + + + ); +} diff --git a/src/app/(main)/teams/TeamsAddButton.tsx b/src/app/(main)/teams/TeamsAddButton.tsx index 578a273a..13873088 100644 --- a/src/app/(main)/teams/TeamsAddButton.tsx +++ b/src/app/(main)/teams/TeamsAddButton.tsx @@ -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 }) { - {({ close }) => } + {({ close }) => } diff --git a/src/app/(main)/teams/TeamsMemberAddButton.tsx b/src/app/(main)/teams/TeamsMemberAddButton.tsx new file mode 100644 index 00000000..f1bbf258 --- /dev/null +++ b/src/app/(main)/teams/TeamsMemberAddButton.tsx @@ -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 ( + + + + + {({ close }) => } + + + + ); +} diff --git a/src/app/(main)/teams/[teamId]/TeamSettings.tsx b/src/app/(main)/teams/[teamId]/TeamSettings.tsx index 3ddbe000..4bbb8905 100644 --- a/src/app/(main)/teams/[teamId]/TeamSettings.tsx +++ b/src/app/(main)/teams/[teamId]/TeamSettings.tsx @@ -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 }) { + + {formatMessage(labels.members)} + {isAdmin && } + {isTeamOwner && ( diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index 53ef5923..c571f405 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -28,6 +28,7 @@ export async function GET(request: Request) { export async function POST(request: Request) { const schema = z.object({ name: z.string().max(50), + ownerId: z.uuid().optional(), }); const { auth, body, error } = await parseRequest(request, schema); @@ -40,7 +41,7 @@ export async function POST(request: Request) { return unauthorized(); } - const { name } = body; + const { name, ownerId } = body; const team = await createTeam( { @@ -48,7 +49,7 @@ export async function POST(request: Request) { name, accessCode: `team_${getRandomChars(16)}`, }, - auth.user.id, + ownerId ?? auth.user.id, ); return json(team); diff --git a/src/components/input/UserSelect.tsx b/src/components/input/UserSelect.tsx new file mode 100644 index 00000000..ccb3d432 --- /dev/null +++ b/src/components/input/UserSelect.tsx @@ -0,0 +1,71 @@ +import { ListItem, Row, Select, type SelectProps, Text } from '@umami/react-zen'; +import { useMemo, useState } from 'react'; +import { Empty } from '@/components/common/Empty'; +import { useMessages, useTeamMembersQuery, useUsersQuery } from '@/components/hooks'; + +export function UserSelect({ + teamId, + onChange, + ...props +}: { + teamId?: string; +} & SelectProps) { + const { formatMessage, messages } = useMessages(); + const { data: users, isLoading: usersLoading } = useUsersQuery(); + const { data: teamMembers, isLoading: teamMembersLoading } = useTeamMembersQuery(teamId); + const [username, setUsername] = useState(); + const [search, setSearch] = useState(''); + + const listItems = useMemo(() => { + if (!users) { + return []; + } + if (!teamId || !teamMembers) { + return users.data; + } + const teamMemberIds = teamMembers.data.map(({ userId }) => userId); + return users.data.filter(({ id }) => !teamMemberIds.includes(id)); + }, [users, teamMembers, teamId]); + + const handleSearch = (value: string) => { + setSearch(value); + }; + + const handleOpenChange = () => { + setSearch(''); + }; + + const handleChange = (id: string) => { + setUsername(listItems.find(item => item.id === id)?.username); + onChange(id); + }; + + const renderValue = () => { + return ( + + {username} + + ); + }; + + return ( + + ); +} diff --git a/src/index.ts b/src/index.ts index 907c5623..df164b9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,11 +19,13 @@ export * from '@/app/(main)/teams/TeamAddForm'; export * from '@/app/(main)/teams/TeamJoinForm'; export * from '@/app/(main)/teams/TeamLeaveButton'; export * from '@/app/(main)/teams/TeamLeaveForm'; +export * from '@/app/(main)/teams/TeamMemberAddForm'; export * from '@/app/(main)/teams/TeamProvider'; export * from '@/app/(main)/teams/TeamsAddButton'; export * from '@/app/(main)/teams/TeamsDataTable'; export * from '@/app/(main)/teams/TeamsHeader'; export * from '@/app/(main)/teams/TeamsJoinButton'; +export * from '@/app/(main)/teams/TeamsMemberAddButton'; export * from '@/app/(main)/teams/TeamsTable'; export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteData'; export * from '@/app/(main)/websites/[websiteId]/settings/WebsiteDeleteForm';