Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Multiple Private links per Event Type #15896

Merged
merged 55 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d5423c6
feat: Change hashedLink field to array
Souptik2001 Jul 23, 2024
3ce34e4
feat: Implement core single-use links logic
Souptik2001 Jul 24, 2024
03a3482
fix: Get private link from list of hashedLinks on event list page
Souptik2001 Jul 24, 2024
42022b8
fix: Fix defalut single-use links form field value
Souptik2001 Jul 24, 2024
4c20826
fix: Fix some type errors while compiling
Souptik2001 Jul 24, 2024
7e872d4
fix: Use onChange callback instead of setting form values directly
Souptik2001 Jul 24, 2024
0542141
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Jul 30, 2024
c6a3248
feat: Add e2e tests for single-use links
Souptik2001 Jul 30, 2024
3e55bc7
refactor: Refactor single-use-links admin interface
Souptik2001 Jul 31, 2024
fc0d68c
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Aug 1, 2024
af7ab3e
fix: Fix settings toggle bugs
Souptik2001 Aug 1, 2024
4269a78
chore: Remove redundant inline comments
Souptik2001 Aug 1, 2024
374cb91
fix: Remove debug statements
Souptik2001 Aug 1, 2024
945ffbc
chore: Remove single-use links e2e tests
Souptik2001 Aug 1, 2024
26b2b0a
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Aug 15, 2024
361253b
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Aug 16, 2024
b50ab5b
feat: Remove private-link feature and override with single-use-links
Souptik2001 Aug 16, 2024
f473e06
fix: Fix type errors
Souptik2001 Aug 16, 2024
3f5d7b8
refactor: Remove redundant migrations
Souptik2001 Aug 16, 2024
16deeb7
fix: Add managedEvents restriction
Souptik2001 Aug 16, 2024
079e9a2
fix: Fix single-use-links for org
Souptik2001 Aug 16, 2024
5c41109
fix: Fix recommended UI changes
Souptik2001 Aug 20, 2024
0cc66d8
fix: Fix duration not respected for private links
Souptik2001 Aug 20, 2024
e95fc3d
fix: Fix team event page title
Souptik2001 Aug 20, 2024
78e8bf7
feat: Add logic to copy each single-use-links from event listing page
Souptik2001 Aug 20, 2024
1dfa548
feat: Add single-use links support for managed event types
Souptik2001 Aug 21, 2024
7d12c42
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Aug 21, 2024
d24cb05
fix: Fix some type errors
Souptik2001 Aug 21, 2024
acde230
fix: Fix type errors
Souptik2001 Aug 21, 2024
198e002
refactor: Disable adding new single-use links when field is locked
Souptik2001 Aug 21, 2024
2add9ba
fix: Address feedbacks
Souptik2001 Aug 21, 2024
0830468
refactor: Hide add new link button for locked single-use link field
Souptik2001 Aug 21, 2024
4ef15a3
chore: Use empty string for single-use link fallback value condition
Souptik2001 Aug 21, 2024
ff19caa
fix: Fix org single-use links in listing page
Souptik2001 Aug 21, 2024
5a2ed6f
feat: Auto toggle unlock button for managed event's single-use links …
Souptik2001 Aug 22, 2024
80cc879
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Aug 25, 2024
394a5b7
Revert "feat: Auto toggle unlock button for managed event's single-us…
Souptik2001 Aug 25, 2024
9f51012
chore: Disable parent toggle for single-use-links
Souptik2001 Aug 25, 2024
ce91302
feat: Remove tooltip and give info message below field
Souptik2001 Aug 25, 2024
0a8e1d3
add tooltip
anikdhabal Aug 25, 2024
b4282c0
tooltip message
anikdhabal Aug 25, 2024
87e2363
fix: Fix typo
Souptik2001 Aug 26, 2024
f5b72c7
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Aug 29, 2024
cd9e05e
fix: Fix typescript type errors
Souptik2001 Aug 29, 2024
d083d98
fix: Fix few unit tests
Souptik2001 Aug 29, 2024
e36cfb2
refactor: Remove redundant query
Souptik2001 Aug 30, 2024
86a78e9
Merge branch 'main' of https://github.com/calcom/cal.com into pr/15896
Amit91848 Sep 22, 2024
25b2030
merge conflict fix
Amit91848 Sep 22, 2024
28ac71d
More merge conflict fix and refactor
Amit91848 Sep 22, 2024
a8effb9
fix tests, code refactor
Amit91848 Sep 22, 2024
dd65bc3
fix: type check
Amit91848 Sep 22, 2024
182eacc
fix: duration config and keep links when updating event type
Amit91848 Sep 22, 2024
ad7a752
fix: e2e
Amit91848 Sep 22, 2024
a3dcd2f
refactor: Replace single-use-links with multiple-private-links
Souptik2001 Sep 28, 2024
f6588a0
Merge branch 'main' into feature-15776-single-use-link
Souptik2001 Oct 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/lib/d/[link]/[slug]/getServerSideProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
queryDuration,
eventData.length
),
durationConfig: eventData.metadata?.multipleDuration ?? [],
anikdhabal marked this conversation as resolved.
Show resolved Hide resolved
booking,
user: name,
slug,
Expand Down
27 changes: 22 additions & 5 deletions apps/web/modules/event-types/views/event-types-listing-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF
import Shell from "@calcom/features/shell/Shell";
import { parseEventTypeColor } from "@calcom/lib";
import { APP_NAME } from "@calcom/lib/constants";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
Expand Down Expand Up @@ -310,6 +309,7 @@ export const EventTypeList = ({
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
null
);
const [privateLinkCopyIndices, setPrivateLinkCopyIndices] = useState<Record<string, number>>({});
const utils = trpc.useUtils();
const mutation = trpc.viewer.eventTypeOrder.useMutation({
onError: async (err) => {
Expand Down Expand Up @@ -477,8 +477,11 @@ export const EventTypeList = ({
{types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${bookerUrl}/${embedLink}`;
const isPrivateURLEnabled = type.hashedLink?.link;
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
const isPrivateURLEnabled =
type.hashedLink && type.hashedLink.length > 0
? type.hashedLink[privateLinkCopyIndices[type.slug] ?? 0]?.link
: "";
const placeholderHashedLink = `${bookerUrl}/d/${isPrivateURLEnabled}/${type.slug}`;
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType =
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
Expand Down Expand Up @@ -567,6 +570,11 @@ export const EventTypeList = ({
onClick={() => {
showToast(t("private_link_copied"), "success");
navigator.clipboard.writeText(placeholderHashedLink);
setPrivateLinkCopyIndices((prev) => {
const prevIndex = prev[type.slug] ?? 0;
prev[type.slug] = (prevIndex + 1) % type.hashedLink.length;
return prev;
});
}}
/>
</Tooltip>
Expand Down Expand Up @@ -814,6 +822,7 @@ export const InfiniteEventTypeList = ({
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
null
);
const [privateLinkCopyIndices, setPrivateLinkCopyIndices] = useState<Record<string, number>>({});

const utils = trpc.useUtils();
const mutation = trpc.viewer.eventTypeOrder.useMutation({
Expand Down Expand Up @@ -1007,8 +1016,11 @@ export const InfiniteEventTypeList = ({
return page?.eventTypes?.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${bookerUrl}/${embedLink}`;
const isPrivateURLEnabled = type.hashedLink?.link;
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
const isPrivateURLEnabled =
type.hashedLink && type.hashedLink.length > 0
? type.hashedLink[privateLinkCopyIndices[type.slug] ?? 0]?.link
: "";
const placeholderHashedLink = `${bookerUrl}/d/${isPrivateURLEnabled}/${type.slug}`;
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType =
type.metadata?.managedEventConfig !== undefined &&
Expand Down Expand Up @@ -1108,6 +1120,11 @@ export const InfiniteEventTypeList = ({
onClick={() => {
showToast(t("private_link_copied"), "success");
navigator.clipboard.writeText(placeholderHashedLink);
setPrivateLinkCopyIndices((prev) => {
const prevIndex = prev[type.slug] ?? 0;
prev[type.slug] = (prevIndex + 1) % type.hashedLink.length;
return prev;
});
}}
/>
</Tooltip>
Expand Down
20 changes: 12 additions & 8 deletions apps/web/playwright/organization/organization-redirection.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,11 @@ test.describe("Unpublished Organization Redirection", () => {
},
data: {
hashedLink: {
create: {
link: generateHashedLink(eventType.id),
},
create: [
{
link: generateHashedLink(eventType.id),
},
],
},
},
include: {
Expand All @@ -158,7 +160,7 @@ test.describe("Unpublished Organization Redirection", () => {
});

await doOnOrgDomain({ page, orgSlug }, async () => {
await page.goto(`/d/${privateEvent.hashedLink?.link}/${privateEvent.slug}`);
await page.goto(`/d/${privateEvent.hashedLink[0]?.link}/${privateEvent.slug}`);

// Expect the empty screen, indicating the event is inaccessible.
await expect(page.getByTestId("empty-screen")).toBeVisible();
Expand All @@ -180,9 +182,11 @@ test.describe("Unpublished Organization Redirection", () => {
},
data: {
hashedLink: {
create: {
link: generateHashedLink(eventType.id),
},
create: [
{
link: generateHashedLink(eventType.id),
},
],
},
},
include: {
Expand All @@ -191,7 +195,7 @@ test.describe("Unpublished Organization Redirection", () => {
});

await doOnOrgDomain({ page, orgSlug }, async () => {
await page.goto(`/d/${privateEvent.hashedLink?.link}/${privateEvent.slug}?orgRedirection=true`);
await page.goto(`/d/${privateEvent.hashedLink[0]?.link}/${privateEvent.slug}?orgRedirection=true`);

// Verify that the event page is visible.
await expect(page.getByTestId("event-title")).toBeVisible();
Expand Down
5 changes: 5 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -876,12 +876,17 @@
"copy_private_link": "Copy private link",
"copy_private_link_to_event": "Copy private link to event",
"private_link_description": "Generate a private URL to share without exposing your {{appName}} username",
"single_use_links_title": "Single Use Links",
"single_use_links_description": "Generate private URLs without exposing the username, which will be destroyed once used",
"add_a_single_use_link": "Add new link",
"single_use_link_copied": "Single use link copied",
"invitees_can_schedule": "Invitees can schedule",
"date_range": "Date Range",
"calendar_days": "calendar days",
"business_days": "business days",
"set_address_place": "Set an address or place",
"set_link_meeting": "Set a link to the meeting",
"managed_event_field_parent_control_disabled": "Can't be toggled. It can only be unlocked for child event types",
"cal_invitee_phone_number_scheduling": "{{appName}} will ask your invitee to enter a phone number before scheduling.",
"cal_provide_google_meet_location": "{{appName}} will provide a Google Meet location.",
"cal_provide_zoom_meeting_url": "{{appName}} will provide a Zoom meeting URL.",
Expand Down
45 changes: 21 additions & 24 deletions apps/web/test/lib/handleChildrenEventTypes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ describe("handleChildrenEventTypes", () => {
children: [],
updatedEventType: { schedulingType: null, slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand All @@ -64,8 +63,7 @@ describe("handleChildrenEventTypes", () => {
children: [],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand All @@ -91,8 +89,7 @@ describe("handleChildrenEventTypes", () => {
children: [],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand Down Expand Up @@ -130,8 +127,7 @@ describe("handleChildrenEventTypes", () => {
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand Down Expand Up @@ -184,8 +180,7 @@ describe("handleChildrenEventTypes", () => {
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: "somestring",
connectedLink: null,
singleUseLinks: ["somestring"],
prisma: prismaMock,
profileId: null,
updatedValues: {
Expand All @@ -200,8 +195,10 @@ describe("handleChildrenEventTypes", () => {
scheduleId: null,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
hashedLink: {
deleteMany: {},
},
instantMeetingScheduleId: undefined,
hashedLink: { create: { link: expect.any(String) } },
},
where: {
userId_parentId: {
Expand All @@ -224,8 +221,7 @@ describe("handleChildrenEventTypes", () => {
children: [],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand All @@ -250,8 +246,7 @@ describe("handleChildrenEventTypes", () => {
],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand Down Expand Up @@ -290,8 +285,7 @@ describe("handleChildrenEventTypes", () => {
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: ["something"] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand All @@ -305,12 +299,12 @@ describe("handleChildrenEventTypes", () => {
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
hashedLink: undefined,
instantMeetingScheduleId: undefined,
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
userId: 4,
workflows: undefined,
hashedLink: undefined,
},
});
expect(result.newUserIds).toEqual([4]);
Expand Down Expand Up @@ -345,8 +339,7 @@ describe("handleChildrenEventTypes", () => {
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: ["something"] } }],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {
Expand All @@ -358,6 +351,9 @@ describe("handleChildrenEventTypes", () => {
data: {
...rest,
locations: [],
hashedLink: {
deleteMany: {},
},
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
instantMeetingScheduleId: undefined,
Expand Down Expand Up @@ -411,8 +407,7 @@ describe("handleChildrenEventTypes", () => {
],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
singleUseLinks: [],
prisma: prismaMock,
profileId: null,
updatedValues: {},
Expand All @@ -424,7 +419,6 @@ describe("handleChildrenEventTypes", () => {
durationLimits: undefined,
recurringEvent: undefined,
eventTypeColor: undefined,
hashedLink: undefined,
instantMeetingScheduleId: undefined,
locations: [],
lockTimeZoneToggleOnBookingPage: false,
Expand All @@ -441,6 +435,7 @@ describe("handleChildrenEventTypes", () => {
workflows: {
create: [{ workflowId: 11 }],
},
hashedLink: undefined,
},
});
const { profileId, ...rest } = evType;
Expand All @@ -451,7 +446,9 @@ describe("handleChildrenEventTypes", () => {
locations: [],
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
hashedLink: undefined,
hashedLink: {
deleteMany: {},
},
instantMeetingScheduleId: undefined,
},
where: {
Expand Down
12 changes: 2 additions & 10 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1733,20 +1733,12 @@ async function handler(
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
}

// Avoid passing referencesToCreate with id unique constrain values
// refresh hashed link if used
const urlSeed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}`;
const hashedUid = translator.fromUUID(uuidv5(urlSeed, uuidv5.URL));

try {
if (hasHashedBookingLink) {
await prisma.hashedLink.update({
if (hasHashedBookingLink && reqBody.hashedLink) {
await prisma.hashedLink.delete({
where: {
link: reqBody.hashedLink as string,
},
data: {
link: hashedUid,
},
});
}
} catch (error) {
Expand Down
Loading
Loading