Skip to content

Commit

Permalink
feat: Teams Page (#480)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankParticle authored May 27, 2024
1 parent d036ab9 commit 62a32bb
Show file tree
Hide file tree
Showing 11 changed files with 963 additions and 8 deletions.
73 changes: 73 additions & 0 deletions apps/platform/trpc/routers/orgRouter/users/teamsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ export const teamsRouter = router({
}
}
}
},
authorizedEmailIdentities: {
columns: {},
with: {
emailIdentity: {
columns: {
username: true,
sendName: true,
domainName: true
}
}
}
}
}
});
Expand Down Expand Up @@ -199,5 +211,66 @@ export const teamsRouter = router({
return {
publicId: newTeamMemberPublicId
};
}),
updateTeamMembers: orgProcedure
.input(
z.object({
teamPublicId: typeIdValidator('teams'),
orgMemberPublicIds: z.array(typeIdValidator('orgMembers'))
})
)
.mutation(async ({ ctx, input }) => {
if (!ctx.account || !ctx.org) {
throw new TRPCError({
code: 'UNPROCESSABLE_CONTENT',
message: 'Account or Organization is not defined'
});
}
const { org, db } = ctx;
const { teamPublicId, orgMemberPublicIds } = input;

const isAdmin = await isAccountAdminOfOrg(org);
if (!isAdmin) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You are not an admin'
});
}
const teamMembers = await db.query.teams.findFirst({
where: and(eq(teams.publicId, teamPublicId), eq(teams.orgId, org.id)),
columns: {},
with: {
members: {
columns: {},
with: {
orgMember: {
columns: {
publicId: true
}
}
}
}
}
});
if (!teamMembers) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Team not found'
});
}
const currentMembers = teamMembers.members.map(
(m) => m.orgMember.publicId
);
const newMembers = orgMemberPublicIds.filter(

Check warning on line 264 in apps/platform/trpc/routers/orgRouter/users/teamsRouter.ts

View workflow job for this annotation

GitHub Actions / Check and Build

'newMembers' is assigned a value but never used
(m) => !currentMembers.includes(m)
);
const removedMembers = currentMembers.filter(

Check warning on line 267 in apps/platform/trpc/routers/orgRouter/users/teamsRouter.ts

View workflow job for this annotation

GitHub Actions / Check and Build

'removedMembers' is assigned a value but never used
(m) => !orgMemberPublicIds.includes(m)
);

throw new TRPCError({
code: 'NOT_IMPLEMENTED',
message: 'Not implemented'
});
})
});
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,15 @@ export function InviteModal() {
<div className="flex gap-1">
<form.Field
name="email.address"
validators={{ onBlur: z.string().min(1).max(64) }}
validators={{
onBlur: z
.string()
.min(1)
.max(32)
.regex(/^[a-zA-Z0-9._-]*$/, {
message: 'Only letters and numbers'
})
}}
children={(field) => (
<Input
className="w-full flex-1"
Expand All @@ -322,6 +330,7 @@ export function InviteModal() {
<span className="flex items-center">@</span>
<form.Field
name="email.domain"
validators={{ onChange: z.string().min(1) }}
children={(field) => (
<Select
name={field.name}
Expand Down Expand Up @@ -423,7 +432,7 @@ export function InviteModal() {
<div
className="h-4 w-4 rounded-full border"
style={{
backgroundColor: item.color ?? undefined
backgroundColor: `var(--${item.color}-10)`
}}
/>
{item.name}
Expand All @@ -438,7 +447,7 @@ export function InviteModal() {
<div
className="h-4 w-4 rounded-full border"
style={{
backgroundColor: item.color ?? undefined
backgroundColor: `var(--${item.color}-10)`
}}
/>
<div>{item.name}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Button } from '@/src/components/shadcn-ui/button';
import { api } from '@/src/lib/trpc';
import { useGlobalStore } from '@/src/providers/global-store-provider';
import { type TypeId } from '@u22n/utils/typeid';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/src/components/shadcn-ui/select';
import { useState } from 'react';
import { toast } from 'sonner';

type Props = {
teamId: TypeId<'teams'>;
existingMembers: TypeId<'orgMembers'>[];
complete: () => Promise<void>;
};

export function AddNewMember({ teamId, existingMembers, complete }: Props) {
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
const { data: allMembers, isLoading } =
api.org.users.members.getOrgMembers.useQuery({
orgShortCode
});
const { mutateAsync: addNewMember, isLoading: isAdding } =
api.org.users.teams.addOrgMemberToTeam.useMutation({
onError: (error) => {
toast.error(error.message);
}
});
const [selectedMember, setSelectedMember] = useState('');

return isLoading ? (
<div className="font-bold">Loading...</div>
) : (
<div className="flex w-fit flex-col gap-2">
<div className="font-bold">Add a new Member</div>
<Select
value={selectedMember}
onValueChange={setSelectedMember}>
<SelectTrigger>
<SelectValue placeholder="Select a Member" />
</SelectTrigger>
<SelectContent>
{allMembers?.members
?.filter((m) => !existingMembers.includes(m.publicId))
.map((m) => (
<SelectItem
key={m.publicId}
value={m.publicId}>
{`${m.profile.firstName} ${m.profile.lastName}`}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
className="w-fit"
disabled={!selectedMember || isAdding}
// loading={isAdding}
onClick={async () => {
await addNewMember({
orgShortCode,
teamPublicId: teamId,
orgMemberPublicId: selectedMember
});
await complete();
}}>
{isAdding ? 'Adding...' : 'Add Member'}
</Button>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

import { createColumnHelper, type ColumnDef } from '@tanstack/react-table';
import type { RouterOutputs } from '@/src/lib/trpc';
import { generateAvatarUrl, getInitials } from '@/src/lib/utils';
import {
Avatar,
AvatarFallback,
AvatarImage
} from '@/src/components/shadcn-ui/avatar';

type Member =
// eslint-disable-next-line @typescript-eslint/ban-types
(RouterOutputs['org']['users']['teams']['getTeam']['team'] & {})['members'][number];

const columnHelper = createColumnHelper<Member>();

export const columns: ColumnDef<Member>[] = [
columnHelper.display({
id: 'name',
header: 'Name',
cell: ({ row }) => {
if (!row.original.orgMemberProfile) return null;
const { publicId, avatarTimestamp, firstName, lastName } =
row.original.orgMemberProfile;

const avatarUrl = generateAvatarUrl({
avatarTimestamp,
publicId,
size: 'lg'
});
const initials = getInitials(`${firstName} ${lastName}`);

return (
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarImage
src={avatarUrl ?? undefined}
alt={`${firstName} ${lastName}`}
/>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<span>{`${firstName} ${lastName}`}</span>
</div>
);
}
}),
columnHelper.display({
id: 'username',
header: 'Username',
cell: ({ row }) => {
return (
<div className="flex h-full items-center">
@{row.original.orgMemberProfile?.handle ?? null}
</div>
);
}
}),
columnHelper.display({
id: 'title',
header: 'Title',
cell: ({ row }) => {
return (
<div className="flex h-full items-center">
{row.original.orgMemberProfile?.title ?? null}
</div>
);
}
})
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* This Component is ready but the backend is yet to implemented, so we are currently using a simpler one member at a time setup until the backend is ready
*/

import { Button } from '@/src/components/shadcn-ui/button';
import { MultiSelect } from '@/src/components/shared/multiselect';
import { api } from '@/src/lib/trpc';
import { useGlobalStore } from '@/src/providers/global-store-provider';
import { type TypeId } from '@u22n/utils/typeid';
import { useState, useEffect } from 'react';

type Props = {
teamId: TypeId<'teams'>;
existingMembers: TypeId<'orgMembers'>[];
complete: () => void;
};

export function EditMemberList({ teamId, existingMembers, complete }: Props) {
const orgShortCode = useGlobalStore((state) => state.currentOrg.shortCode);
const { data: allMembers, isLoading } =
api.org.users.members.getOrgMembers.useQuery({
orgShortCode
});
const { mutateAsync: saveList } =
api.org.users.teams.updateTeamMembers.useMutation();

const [selectedMembers, setSelectedMembers] = useState<string[]>([]);

useEffect(() => {
if (!allMembers) return;
setSelectedMembers(
allMembers.members
?.filter((member) =>
existingMembers.some(
(existingMember) => existingMember === member.publicId
)
)
.map((m) => m.publicId) ?? []
);
}, [allMembers, existingMembers]);

return isLoading ? (
<div className="font-bold">Loading...</div>
) : (
<div className="flex flex-col gap-2">
<div className="font-bold">Edit Members List</div>
<MultiSelect
items={
allMembers?.members?.map((m) => ({
name: `${m.profile.firstName} ${m.profile.lastName}`,
value: m.publicId,
keywords: [m.profile.handle ?? '', m.profile.title ?? '']
})) ?? []
}
ItemRenderer={(item) => <div>{item.name}</div>}
values={selectedMembers}
setValues={setSelectedMembers}
TriggerRenderer={(props) => (
<div className="flex flex-wrap gap-1">
{props.items.map((item, i, all) => (
<span key={item.value}>
{item.name}
{i !== all.length - 1 && ', '}
</span>
))}
</div>
)}
/>
<Button
className="w-fit"
onClick={async () => {
await saveList({
orgShortCode,
teamPublicId: teamId,
orgMemberPublicIds: selectedMembers
});
complete();
}}>
Save
</Button>
</div>
);
}
Loading

0 comments on commit 62a32bb

Please sign in to comment.