Skip to content

Commit 3204d0a

Browse files
feat: team admin: see connected apps of team members (#11036)
* added feature:team admin can see connected apps of members * fixed the type error * Update packages/lib/server/queries/teams/index.ts * Minor fixes --------- Co-authored-by: alannnc <[email protected]>
1 parent 25684f9 commit 3204d0a

File tree

2 files changed

+68
-13
lines changed

2 files changed

+68
-13
lines changed

packages/features/ee/teams/components/MemberListItem.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,22 @@ export default function MemberListItem(props: Props) {
109109
const bookerUrl = useBookerUrl();
110110
const bookerUrlWithoutProtocol = bookerUrl.replace(/^https?:\/\//, "");
111111
const bookingLink = !!props.member.username && `${bookerUrlWithoutProtocol}/${props.member.username}`;
112-
112+
const isAdmin = props.team && ["ADMIN", "OWNER"].includes(props.team.membership?.role);
113+
const appList = props.member.connectedApps.map(({ logo, name, externalId }) => {
114+
return logo ? (
115+
externalId ? (
116+
<div className="ltr:mr-2 rtl:ml-2 ">
117+
<Tooltip content={externalId}>
118+
<img className="h-5 w-5" src={logo} alt={`${name} logo`} />
119+
</Tooltip>
120+
</div>
121+
) : (
122+
<div className="ltr:mr-2 rtl:ml-2">
123+
<img className="h-5 w-5" src={logo} alt={`${name} logo`} />
124+
</div>
125+
)
126+
) : null;
127+
});
113128
return (
114129
<li className="divide-subtle divide-y px-5">
115130
<div className="my-4 flex justify-between">
@@ -124,9 +139,9 @@ export default function MemberListItem(props: Props) {
124139

125140
<div className="ms-3 inline-block">
126141
<div className="mb-1 flex">
127-
<span className="text-default mr-1 text-sm font-bold leading-4">{name}</span>
128-
142+
<span className="text-default mr-2 text-sm font-bold leading-4">{name}</span>
129143
{!props.member.accepted && <TeamPill color="orange" text={t("pending")} />}
144+
{isAdmin && props.member.accepted && appList}
130145
{props.member.role && <TeamRole role={props.member.role} />}
131146
</div>
132147
<div className="text-default flex items-center">

packages/lib/server/queries/teams/index.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Prisma } from "@prisma/client";
22

3+
import { getAppFromSlug } from "@calcom/app-store/utils";
4+
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
35
import { getSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
46
import prisma, { baseEventTypeSelect } from "@calcom/prisma";
5-
import { SchedulingType } from "@calcom/prisma/enums";
7+
import { AppCategories, SchedulingType } from "@calcom/prisma/enums";
68
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
79

810
import { WEBAPP_URL } from "../../../constants";
@@ -22,6 +24,17 @@ export async function getTeamWithMembers(args: {
2224
name: true,
2325
id: true,
2426
bio: true,
27+
destinationCalendar: {
28+
select: {
29+
externalId: true,
30+
},
31+
},
32+
selectedCalendars: true,
33+
credentials: {
34+
include: {
35+
app: true,
36+
},
37+
},
2538
});
2639
const teamSelect = Prisma.validator<Prisma.TeamSelect>()({
2740
id: true,
@@ -111,16 +124,43 @@ export async function getTeamWithMembers(args: {
111124
});
112125

113126
if (!team) return null;
114-
const members = team.members.map((obj) => {
115-
return {
116-
...obj.user,
117-
role: obj.role,
118-
accepted: obj.accepted,
119-
disableImpersonation: obj.disableImpersonation,
120-
avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`,
121-
};
122-
});
127+
const members = await Promise.all(
128+
team.members.map(async (obj) => {
129+
const calendarCredentials = getCalendarCredentials(obj.user.credentials);
123130

131+
const { connectedCalendars } = await getConnectedCalendars(
132+
calendarCredentials,
133+
obj.user.selectedCalendars,
134+
obj.user.destinationCalendar?.externalId
135+
);
136+
const connectedApps = obj.user.credentials
137+
.map(({ app, id }) => {
138+
const appMetaData = getAppFromSlug(app?.slug);
139+
140+
if (app?.categories.includes(AppCategories.calendar)) {
141+
const externalId = connectedCalendars.find((cal) => cal.credentialId == id)?.primary?.email;
142+
return { name: appMetaData?.name, logo: appMetaData?.logo, slug: appMetaData?.slug, externalId };
143+
}
144+
return { name: appMetaData?.name, logo: appMetaData?.logo, slug: appMetaData?.slug };
145+
})
146+
.sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""));
147+
// Prevent credentials from leaking to frontend
148+
const {
149+
credentials: _credentials,
150+
destinationCalendar: _destinationCalendar,
151+
selectedCalendars: _selectedCalendars,
152+
...rest
153+
} = {
154+
...obj.user,
155+
role: obj.role,
156+
accepted: obj.accepted,
157+
disableImpersonation: obj.disableImpersonation,
158+
avatar: `${WEBAPP_URL}/${obj.user.username}/avatar.png`,
159+
connectedApps,
160+
};
161+
return rest;
162+
})
163+
);
124164
const eventTypes = team.eventTypes.map((eventType) => ({
125165
...eventType,
126166
metadata: EventTypeMetaDataSchema.parse(eventType.metadata),

0 commit comments

Comments
 (0)