Improvement/teams (#1285)
* [WIP] checkpoint before pull & merge - Added teams to sidebar - Refactored team settings - Improved team list UI This code will be partly reverted next commit. * [WIP] - Moved team code back to components - Removed team link from sidebar - Built new team manager screen based on Event Type designs - Component-ized frequently reused code (SettingInputContainer, FlatIconButton) * [WIP] - Created LinkIconButton as standalone component - Added functionality to sidebar of team settings - Fixed type bug on public team page induced by my normalization of members array in team query - Removed teams-old which was kept as refrence - Cleaned up loose ends * [WIP] - added create team model - fixed profile missing label due to my removal of default label from component * [WIP] - Fixed TeamCreateModal trigger - removed TeamShell, it didn't make the cut - added getPlaceHolderAvatar - renamed TeamCreate to TeamCreateModal - removed deprecated UsernameInput and replaced uses with suggested TextField * fix save button * [WIP] - Fixed drop down actions on team list - Cleaned up state updates * [WIP] converting teams to tRPC * [WIP] Finished refactor to tRPC * [WIP] Finishing touches * [WIP] Team availability beginning * team availability mvp * - added validation to change role - modified layout of team availability - corrected types * fix ui issue on team availability screen * - added virtualization to team availability - added flexChildrenContainer boolean to Shell to allow for flex on children * availability style fix * removed hard coded team type as teams now use inferred type from tRPC * Removed unneeded vscode settings * Reverted prisma schema * Fixed migrations * Removes unused dayjs plugins * Reverts type regression * Type fix * Type fixes * Type fixes * Moves team availability code to ee Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
49 files changed
+2295
-998
No files matched your search
@@ -0,0 +1,6 @@
|
||||
export function getPlaceholderAvatar(avatar: string | null | undefined, name: string | null) {
|
||||
return avatar
|
||||
? avatar
|
||||
: "https://eu.ui-avatars.com/api/?background=fff&color=f9f9f9&bold=true&background=000000&name=" +
|
||||
encodeURIComponent(name || "");
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// import { getBusyVideoTimes } from "@lib/videoClient";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import { getBusyCalendarTimes } from "@lib/calendarClient";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export async function getUserAvailability(query: {
|
||||
username: string;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
eventTypeId?: number;
|
||||
timezone?: string;
|
||||
}) {
|
||||
const username = asStringOrNull(query.username);
|
||||
const dateFrom = dayjs(asStringOrNull(query.dateFrom));
|
||||
const dateTo = dayjs(asStringOrNull(query.dateTo));
|
||||
|
||||
if (!username) throw new Error("Missing username");
|
||||
if (!dateFrom.isValid() || !dateTo.isValid()) throw new Error("Invalid time range given.");
|
||||
|
||||
const rawUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: username,
|
||||
},
|
||||
select: {
|
||||
credentials: true,
|
||||
timeZone: true,
|
||||
bufferTime: true,
|
||||
availability: true,
|
||||
id: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
selectedCalendars: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getEventType = (id: number) =>
|
||||
prisma.eventType.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
timeZone: true,
|
||||
availability: {
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
days: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
type EventType = Prisma.PromiseReturnType<typeof getEventType>;
|
||||
let eventType: EventType | null = null;
|
||||
if (query.eventTypeId) eventType = await getEventType(query.eventTypeId);
|
||||
|
||||
if (!rawUser) throw new Error("No user found");
|
||||
|
||||
const { selectedCalendars, ...currentUser } = rawUser;
|
||||
|
||||
const busyTimes = await getBusyCalendarTimes(
|
||||
currentUser.credentials,
|
||||
dateFrom.format(),
|
||||
dateTo.format(),
|
||||
selectedCalendars
|
||||
);
|
||||
|
||||
// busyTimes.push(...await getBusyVideoTimes(currentUser.credentials, dateFrom.format(), dateTo.format()));
|
||||
|
||||
const bufferedBusyTimes = busyTimes.map((a) => ({
|
||||
start: dayjs(a.start).subtract(currentUser.bufferTime, "minute").toString(),
|
||||
end: dayjs(a.end).add(currentUser.bufferTime, "minute").toString(),
|
||||
}));
|
||||
|
||||
const timeZone = query.timezone || eventType?.timeZone || currentUser.timeZone;
|
||||
const workingHours = getWorkingHours(
|
||||
{ timeZone },
|
||||
eventType?.availability.length ? eventType.availability : currentUser.availability
|
||||
);
|
||||
|
||||
return {
|
||||
busy: bufferedBusyTimes,
|
||||
timeZone,
|
||||
workingHours,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (...args: any) => Promise<infer R>
|
||||
? R
|
||||
: any;
|
||||
|
||||
export type TeamWithMembers = AsyncReturnType<typeof getTeamWithMembers>;
|
||||
|
||||
export async function getTeamWithMembers(id?: number, slug?: string) {
|
||||
const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
username: true,
|
||||
avatar: true,
|
||||
email: true,
|
||||
name: true,
|
||||
id: true,
|
||||
bio: true,
|
||||
});
|
||||
|
||||
const teamSelect = Prisma.validator<Prisma.TeamSelect>()({
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
logo: true,
|
||||
bio: true,
|
||||
hideBranding: true,
|
||||
members: {
|
||||
select: {
|
||||
user: {
|
||||
select: userSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
eventTypes: {
|
||||
where: {
|
||||
hidden: false,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
length: true,
|
||||
slug: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
users: {
|
||||
select: userSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: id ? { id } : { slug },
|
||||
select: teamSelect,
|
||||
});
|
||||
|
||||
if (!team) return null;
|
||||
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
const members = team.members.map((obj) => {
|
||||
const membership = memberships.find((membership) => obj.user.id === membership.userId);
|
||||
return {
|
||||
...obj.user,
|
||||
role: membership?.role,
|
||||
accepted: membership?.role === "OWNER" ? true : membership?.accepted,
|
||||
};
|
||||
});
|
||||
|
||||
return { ...team, members };
|
||||
}
|
||||
// also returns team
|
||||
export async function isTeamAdmin(userId: number, teamId: number) {
|
||||
return (
|
||||
(await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
OR: [{ role: "ADMIN" }, { role: "OWNER" }],
|
||||
},
|
||||
})) || false
|
||||
);
|
||||
}
|
||||
export async function isTeamOwner(userId: number, teamId: number) {
|
||||
return !!(await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: "OWNER",
|
||||
},
|
||||
}));
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
export interface Team {
|
||||
id: number;
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
logo: string | null;
|
||||
bio: string | null;
|
||||
role: string | null;
|
||||
hideBranding: boolean;
|
||||
prevState: null;
|
||||
}
|
||||
Reference in new issue
Block a user