From 9322b4ab4c0db7b38d4e507bbfe20d958e9dbbe4 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Mon, 9 May 2022 16:41:07 +0530 Subject: [PATCH] Flow, UX and other improvements for hash my url feature (#2644) * added toast feedback * updated flow * locale * updated locale data * removed unused booking call for reschedule flow * fixed hashedURL test * test adjustment * further test changes * added check in test to click check only if unchecked * Added private link quick copy button * fixed spacing * fix lint * consistency * moved create hash function out of component render Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/pages/event-types/[type].tsx | 58 ++++++++++++++----- apps/web/playwright/hash-my-url.test.ts | 20 +++---- apps/web/public/static/locales/en/common.json | 7 ++- apps/web/server/routers/viewer/eventTypes.tsx | 11 +--- 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index 6fb0b1e8..0f6ff47a 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -26,7 +26,9 @@ import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { Controller, Noop, useForm, UseFormReturn } from "react-hook-form"; import { FormattedNumber, IntlProvider } from "react-intl"; +import short, { generate } from "short-uuid"; import { JSONObject } from "superjson/dist/types"; +import { v5 as uuidv5 } from "uuid"; import { z } from "zod"; import { SelectGifInput } from "@calcom/app-store/giphy/components"; @@ -281,6 +283,14 @@ const EventTypePage = (props: inferSSRProps) => { ); const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink); + const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link); + + const generateHashedLink = (id: number) => { + const translator = short(); + const seed = `${id}:${new Date().getTime()}`; + const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL)); + return uid; + }; useEffect(() => { const fetchTokens = async () => { @@ -315,6 +325,8 @@ const EventTypePage = (props: inferSSRProps) => { console.log(tokensList); // Just here to make sure it passes the gc hook. Can remove once actual use is made of tokensList. fetchTokens(); + + !hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0].id)); }, []); async function deleteEventTypeHandler(event: React.MouseEvent) { @@ -461,9 +473,7 @@ const EventTypePage = (props: inferSSRProps) => { team ? `team/${team.slug}` : eventType.users[0].username }/${eventType.slug}`; - const placeholderHashedLink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/d/${ - eventType.hashedLink ? eventType.hashedLink.link : "xxxxxxxxxxxxxxxxx" - }/${eventType.slug}`; + const placeholderHashedLink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/d/${hashedUrl}/${eventType.slug}`; const mapUserToValue = ({ id, @@ -495,7 +505,7 @@ const EventTypePage = (props: inferSSRProps) => { currency: string; hidden: boolean; hideCalendarNotes: boolean; - hashedLink: boolean; + hashedLink: string | undefined; locations: { type: LocationType; address?: string; link?: string }[]; customInputs: EventTypeCustomInput[]; users: string[]; @@ -1368,27 +1378,31 @@ const EventTypePage = (props: inferSSRProps) => { ( <> { setHashedLinkVisible(e?.target.checked); - formMethods.setValue("hashedLink", e?.target.checked); + formMethods.setValue( + "hashedLink", + e?.target.checked ? hashedUrl : undefined + ); }} /> {hashedLinkVisible && ( -
+
) => { + {hashedLinkVisible && ( + + )} { await page.waitForSelector('//*[@data-testid="show-advanced-settings"]'); await page.click('//*[@data-testid="show-advanced-settings"]'); // we wait for the hashedLink setting to load - await page.waitForSelector('//*[@id="hashedLink"]'); - await page.click('//*[@id="hashedLink"]'); + await page.waitForSelector('//*[@id="hashedLinkCheck"]'); + // ignore if it is already checked, and click if unchecked + const isChecked = await page.isChecked('//*[@id="hashedLinkCheck"]'); + !isChecked && (await page.click('//*[@id="hashedLinkCheck"]')); + // we wait for the hashedLink setting to load + await page.waitForSelector('//*[@data-testid="generated-hash-url"]'); + $url = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue(); // click update await page.focus('//button[@type="submit"]'); await page.keyboard.press("Enter"); }); test("book using generated url hash", async ({ page }) => { - // await page.pause(); - await page.goto("/event-types"); - // We wait until loading is finished - await page.waitForSelector('[data-testid="event-types"]'); - await page.click('//ul[@data-testid="event-types"]/li[1]'); - // We wait for the page to load - await page.waitForSelector('//*[@data-testid="show-advanced-settings"]'); - await page.click('//*[@data-testid="show-advanced-settings"]'); - // we wait for the hashedLink setting to load - await page.waitForSelector('//*[@data-testid="generated-hash-url"]'); - $url = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue(); await page.goto($url); await selectFirstAvailableTimeSlotNextMonth(page); await bookTimeSlot(page); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 0bd6a2f6..8ecef567 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -500,6 +500,7 @@ "user_from_team": "{{user}} from {{team}}", "preview": "Preview", "link_copied": "Link copied!", + "private_link_copied": "Private link copied!", "link_shared": "Link shared!", "title": "Title", "description": "Description", @@ -614,8 +615,9 @@ "starting": "Starting", "disable_guests": "Disable Guests", "disable_guests_description": "Disable adding additional guests while booking.", - "hashed_link": "Generate hashed URL", - "hashed_link_description": "Generate a hashed URL to share without exposing your Cal username", + "private_link": "Generate private URL", + "copy_private_link": "Copy private link", + "private_link_description": "Generate a private URL to share without exposing your Cal username", "invitees_can_schedule": "Invitees can schedule", "date_range": "Date Range", "calendar_days": "calendar days", @@ -779,6 +781,7 @@ "you_will_only_view_it_once": "You will not be able to view it again once you close this modal.", "copy_to_clipboard": "Copy to clipboard", "enabled_after_update": "Enabled after update", + "enabled_after_update_description": "The private link will work after saving", "confirm_delete_api_key": "Revoke this API key", "revoke_api_key": "Revoke API key", "api_key_copied": "API key copied!", diff --git a/apps/web/server/routers/viewer/eventTypes.tsx b/apps/web/server/routers/viewer/eventTypes.tsx index a1e81dcd..dd225c18 100644 --- a/apps/web/server/routers/viewer/eventTypes.tsx +++ b/apps/web/server/routers/viewer/eventTypes.tsx @@ -1,6 +1,4 @@ import { EventTypeCustomInput, MembershipRole, PeriodType, Prisma } from "@prisma/client"; -import short from "short-uuid"; -import { v5 as uuidv5 } from "uuid"; import { z } from "zod"; import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; @@ -88,7 +86,7 @@ const EventTypeUpdateInput = _EventTypeModel }), users: z.array(stringOrNumber).optional(), schedule: z.number().optional(), - hashedLink: z.boolean(), + hashedLink: z.string(), }) .partial() .merge( @@ -318,19 +316,16 @@ export const eventTypesRouter = createProtectedRouter() if (hashedLink) { // check if hashed connection existed. If it did, do nothing. If it didn't, add a new connection if (!connectedLink) { - const translator = short(); - const seed = `${input.eventName}:${input.id}:${new Date().getTime()}`; - const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL)); // create a hashed link await ctx.prisma.hashedLink.upsert({ where: { eventTypeId: input.id, }, update: { - link: uid, + link: hashedLink, }, create: { - link: uid, + link: hashedLink, eventType: { connect: { id: input.id }, },