Skip to content

Commit fc4ecf5

Browse files
anikdhabaldevin-ai-integration[bot]hariombalhara
authored
fix: Apply organization brand colors and theme to member personal events (calcom#24456)
* fix: Apply organization brand colors and theme to member personal events - Fetch organization brand colors and theme in getEventTypesFromDB - Override user brand colors/theme with org values for personal events - Apply to booking confirmation pages, user booking pages, and routing forms - Follow same pattern as hideBranding for consistency - Fixes issue where org brand colors only applied to team events Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * fix: Resolve type errors for organization brand color and theme overrides - Added brandColor, darkBrandColor, theme to ProfileRepository findById organization select - Updated test mock to include new organization fields - Ensures consistent organization branding types across all queries - All type errors related to brand color changes now resolved Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * fix: Remove redundant profile assignment causing type errors The spread operator already includes profile when it exists. Explicit assignment causes type conflicts with union types where profile is not guaranteed to exist in all branches. Resolves TypeScript error at bookings-single-view.getServerSideProps.tsx:134 Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * fix: Use eventTypeRaw.profile for organization brand color access Changed eventType.profile to eventTypeRaw.profile in profile object construction to fix TypeScript errors where profile property doesn't exist on transformed eventType object. The eventTypeRaw object maintains the original profile field from the database query. Resolves TypeScript errors at lines 155, 158, 161 in bookings-single-view.getServerSideProps.tsx Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * fix type error * refactor: Extract branding logic into getBrandingForEventType utility - Create getBrandingForEventType utility function that handles organization brand color overrides for both team and personal events - For team events: org branding → team branding → null - For personal events: org branding → user branding → null - Refactor bookings-single-view to use utility function - Update team booking page to apply organization brand overrides - Add organization branding fields to ProfileRepository and team queries Addresses review feedback from @hariombalhara on PR calcom#24456 Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * Update getBranding.ts * Change Set themeBasis to null instead of branding.theme. * refactor: Simplify branding fallback logic to prevent mixing org/team settings - Changed getBrandingForEventType to use either parent or team branding entirely - Simplified team parent branding structure in getServerSideProps - Prevents inconsistent branding when some properties exist in parent but not others - Addresses PR review feedback from hariombalhara Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * addressed review * test: Add comprehensive unit tests for branding utility functions - Add 20 unit tests covering getBrandingForEventType, getBrandingForUser, and getBrandingForTeam - Test organization branding override scenarios for both team and personal events - Test fallback behavior when organization branding is not set - Test handling of null and optional branding properties - Verify theme and color inheritance from parent organizations Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> * test: Simplify branding tests to focus on core scenarios - Reduce from 20 to 8 essential test cases - Focus on org override and fallback behavior for each utility - Remove redundant edge case tests while maintaining full coverage Co-Authored-By: anik@cal.com <adhabal2002@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: hariom@cal.com <hariombalhara@gmail.com>
1 parent 9d45228 commit fc4ecf5

10 files changed

Lines changed: 361 additions & 19 deletions

File tree

apps/web/lib/apps/routing-forms/[...pages]/getServerSidePropsRoutingLink.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,12 @@ export const getServerSideProps = async function getServerSideProps(
8383
props: {
8484
isEmbed,
8585
profile: {
86-
theme: form.user.theme,
87-
brandColor: form.user.brandColor,
88-
darkBrandColor: form.user.darkBrandColor,
86+
theme: formWithUserProfile.user.profile?.organization?.theme ?? formWithUserProfile.user.theme,
87+
brandColor:
88+
formWithUserProfile.user.profile?.organization?.brandColor ?? formWithUserProfile.user.brandColor,
89+
darkBrandColor:
90+
formWithUserProfile.user.profile?.organization?.darkBrandColor ??
91+
formWithUserProfile.user.darkBrandColor,
8992
},
9093
form: await getSerializableForm({ form: enrichFormWithMigrationData(formWithUserProfile) }),
9194
},

apps/web/lib/booking.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export const getEventTypesFromDB = async (id: number) => {
4949
profile: {
5050
select: {
5151
organizationId: true,
52+
organization: {
53+
select: {
54+
brandColor: true,
55+
darkBrandColor: true,
56+
theme: true,
57+
},
58+
},
5259
},
5360
},
5461
teamId: true,
@@ -71,9 +78,15 @@ export const getEventTypesFromDB = async (id: number) => {
7178
slug: true,
7279
name: true,
7380
hideBranding: true,
81+
brandColor: true,
82+
darkBrandColor: true,
83+
theme: true,
7484
parent: {
7585
select: {
7686
hideBranding: true,
87+
brandColor: true,
88+
darkBrandColor: true,
89+
theme: true,
7790
},
7891
},
7992
createdByOAuthClientId: true,
@@ -116,12 +129,11 @@ export const getEventTypesFromDB = async (id: number) => {
116129
}
117130

118131
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
119-
const { profile, ...restEventType } = eventType;
120-
const isOrgTeamEvent = !!eventType?.team && !!profile?.organizationId;
132+
const isOrgTeamEvent = !!eventType?.team && !!eventType.profile?.organizationId;
121133

122134
return {
123135
isDynamic: false,
124-
...restEventType,
136+
...eventType,
125137
bookingFields: getBookingFieldsWithSystemFields({ ...eventType, isOrgTeamEvent }),
126138
metadata,
127139
};

apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booki
77
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
88
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
99
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
10-
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
10+
import { getBrandingForEventType } from "@calcom/features/profile/lib/getBranding";
1111
import { shouldHideBrandingForTeamEvent } from "@calcom/features/profile/lib/hideBranding";
12+
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
1213
import slugify from "@calcom/lib/slugify";
1314
import { prisma } from "@calcom/prisma";
1415
import type { User } from "@calcom/prisma/client";
@@ -30,7 +31,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
3031
const { req, params, query } = context;
3132
const session = await getServerSession({ req });
3233
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(params);
33-
const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting, email } = query;
34+
const { rescheduleUid, isInstantMeeting: queryIsInstantMeeting } = query;
3435
const allowRescheduleForCancelledBooking = query.allowRescheduleForCancelledBooking === "true";
3536
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req, params?.orgSlug);
3637

@@ -133,6 +134,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
133134
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(team.id, "use-api-v2-for-team-slots");
134135
const useApiV2 = teamHasApiV2Route && hasApiV2RouteInEnv();
135136

137+
const branding = getBrandingForEventType({
138+
eventType: {
139+
team: team.parent ?? team,
140+
users: [],
141+
profile: null,
142+
},
143+
});
144+
136145
return {
137146
props: {
138147
useApiV2,
@@ -153,6 +162,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
153162
: getPlaceholderAvatar(team.logoUrl, team.name),
154163
name,
155164
username: orgSlug ?? null,
165+
...branding,
156166
},
157167
title: eventData.title,
158168
users: eventHostsUserData,
@@ -204,6 +214,9 @@ const getTeamWithEventsData = async (
204214
bannerUrl: true,
205215
logoUrl: true,
206216
hideBranding: true,
217+
brandColor: true,
218+
darkBrandColor: true,
219+
theme: true,
207220
organizationSettings: {
208221
select: {
209222
allowSEOIndexing: true,
@@ -214,6 +227,9 @@ const getTeamWithEventsData = async (
214227
logoUrl: true,
215228
name: true,
216229
slug: true,
230+
brandColor: true,
231+
darkBrandColor: true,
232+
theme: true,
217233
eventTypes: {
218234
where: {
219235
slug: meetingSlug,

apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-util
66
import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
77
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
88
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
9+
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
910
import { getDefaultEvent } from "@calcom/features/eventtypes/lib/defaultEvents";
11+
import { getBrandingForEventType } from "@calcom/features/profile/lib/getBranding";
1012
import { shouldHideBrandingForEvent } from "@calcom/features/profile/lib/hideBranding";
1113
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
1214
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
1315
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
14-
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
1516
import prisma from "@calcom/prisma";
1617
import { customInputSchema } from "@calcom/prisma/zod-utils";
1718
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
@@ -115,7 +116,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
115116
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
116117
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
117118

118-
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
119+
eventTypeRaw.users = eventTypeRaw.hosts?.length
119120
? eventTypeRaw.hosts.map((host) => host.user)
120121
: eventTypeRaw.users;
121122

@@ -150,9 +151,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
150151
const profile = {
151152
name: eventType.team?.name || eventType.users[0]?.name || null,
152153
email: eventType.team ? null : eventType.users[0].email || null,
153-
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
154-
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
155-
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
154+
...getBrandingForEventType({ eventType: eventTypeRaw }),
156155
slug: eventType.team?.slug || eventType.users[0]?.username || null,
157156
};
158157

apps/web/modules/users/views/users-public-view.test.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ function mockedUserPageComponentProps(props: Partial<React.ComponentProps<typeof
2020
theme: "dark",
2121
brandColor: "red",
2222
darkBrandColor: "black",
23-
organization: { requestedSlug: "slug", slug: "slug", id: 1 },
23+
organization: {
24+
requestedSlug: "slug",
25+
slug: "slug",
26+
id: 1,
27+
brandColor: null,
28+
darkBrandColor: null,
29+
theme: null,
30+
},
2431
allowSEOIndexing: true,
2532
username: "john",
2633
},

apps/web/server/lib/[user]/getServerSideProps.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import { encode } from "querystring";
44
import type { z } from "zod";
55

66
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
7+
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
78
import { getEventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic";
9+
import { getBrandingForUser } from "@calcom/features/profile/lib/getBranding";
10+
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
811
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
9-
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
1012
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
1113
import logger from "@calcom/lib/logger";
1214
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
1315
import { safeStringify } from "@calcom/lib/safeStringify";
14-
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
1516
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
1617
import { prisma } from "@calcom/prisma";
1718
import type { EventType, User } from "@calcom/prisma/client";
@@ -33,6 +34,9 @@ type UserPageProps = {
3334
requestedSlug: string | null;
3435
slug: string | null;
3536
id: number | null;
37+
brandColor: string | null;
38+
darkBrandColor: string | null;
39+
theme: string | null;
3640
} | null;
3741
allowSEOIndexing: boolean;
3842
username: string | null;
@@ -130,15 +134,18 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
130134

131135
const [user] = usersInOrgContext; //to be used when dealing with single user, not dynamic group
132136

137+
const branding = getBrandingForUser({ user });
138+
133139
const profile = {
134140
name: user.name || user.username || "",
135141
image: getUserAvatarUrl({
136142
avatarUrl: user.avatarUrl,
137143
}),
138-
theme: user.theme,
139-
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
144+
theme: branding.theme,
145+
brandColor: branding.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
140146
avatarUrl: user.avatarUrl,
141-
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
147+
darkBrandColor:
148+
branding.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
142149
allowSEOIndexing: user.allowSEOIndexing ?? true,
143150
username: user.username,
144151
organization: user.profile.organization,

packages/features/eventtypes/lib/defaultEvents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ const commons = {
141141
restrictionScheduleId: null,
142142
useBookerTimezone: false,
143143
profileId: null,
144+
profile: null,
144145
requiresConfirmationWillBlockSlot: false,
145146
canSendCalVideoTranscriptionEmails: false,
146147
instantMeetingExpiryTimeOffsetInSeconds: 0,

0 commit comments

Comments
 (0)