From 433ef9017b482b7eb4461f9909e9cc25cadac5bd Mon Sep 17 00:00:00 2001 From: Harsh Tandiya Date: Sat, 3 Oct 2026 11:25:45 +0530 Subject: [PATCH] refactor(events): drop link auto-suggest and keep http links as typed The link dialog no longer guesses a label and icon from the URL; new links start on the generic link icon and organisers name them. Editing an existing http:// link saves it back as http:// instead of upgrading it to https://. The server-side URL check names the row instead of its label, since validate runs before the mandatory check and the label may still be empty. --- buzz/events/doctype/buzz_event/buzz_event.py | 2 +- .../dashboard/events/EventLinkDialog.vue | 33 ++++--------------- .../src/pages/manage/events/EventDetails.vue | 2 +- dashboard/src/utils/eventLinks.test.ts | 21 +----------- dashboard/src/utils/eventLinks.ts | 21 ------------ e2e/tests/manage-event.spec.ts | 3 +- 6 files changed, 12 insertions(+), 70 deletions(-) diff --git a/buzz/events/doctype/buzz_event/buzz_event.py b/buzz/events/doctype/buzz_event/buzz_event.py index ee2a1bec..c538d894 100644 --- a/buzz/events/doctype/buzz_event/buzz_event.py +++ b/buzz/events/doctype/buzz_event/buzz_event.py @@ -153,7 +153,7 @@ def validate_co_hosts(self): def validate_external_links(self): for row in self.external_links: if urlparse(row.url or "").scheme not in ("http", "https"): - frappe.throw(_("Link {0} must start with http:// or https://").format(row.label)) + frappe.throw(_("Row {0}: link must start with http:// or https://").format(row.idx)) def set_time_zone_label(self): # validate runs before the mandatory check, so dates may still be empty here diff --git a/dashboard/src/components/dashboard/events/EventLinkDialog.vue b/dashboard/src/components/dashboard/events/EventLinkDialog.vue index b358c785..a72d827b 100644 --- a/dashboard/src/components/dashboard/events/EventLinkDialog.vue +++ b/dashboard/src/components/dashboard/events/EventLinkDialog.vue @@ -5,7 +5,7 @@ import { computed, reactive, ref, watch } from "vue" import { stripUrlScheme } from "@/components/dashboard/sponsorships/helpers" import WebsiteInput from "@/components/dashboard/sponsorships/WebsiteInput.vue" import type { EventExternalLink } from "@/types" -import { LINK_ICONS, isValidUrl, normalizeUrl, suggestLink } from "@/utils/eventLinks" +import { LINK_ICONS, isValidUrl } from "@/utils/eventLinks" const props = defineProps<{ link: EventExternalLink | null }>() const isOpen = defineModel({ required: true }) @@ -13,34 +13,20 @@ const emit = defineEmits<{ submit: [link: EventExternalLink]; remove: [] }>() const draft = reactive({ icon: "", label: "", url: "" }) const showErrors = ref(false) -let suggestedLabel = "" -let iconChosen = false +// WebsiteInput drops the scheme, so an existing http:// link would otherwise save back as https://. +let scheme = "https://" watch(isOpen, (open) => { if (!open) return Object.assign(draft, { - icon: props.link?.icon ?? "", + icon: props.link?.icon || "link", label: props.link?.label ?? "", url: stripUrlScheme(props.link?.url ?? ""), }) showErrors.value = false - suggestedLabel = "" - iconChosen = !!props.link + scheme = /^http:\/\//i.test(props.link?.url ?? "") ? "http://" : "https://" }) -watch( - () => draft.url, - (url) => { - const suggestion = suggestLink(url) - if (!suggestion) return - if (!draft.label || draft.label === suggestedLabel) { - draft.label = suggestion.label - suggestedLabel = suggestion.label - } - if (!iconChosen) draft.icon = suggestion.icon - }, -) - const urlError = computed(() => { if (!draft.url.trim()) return __("Add the address people should open.") return isValidUrl(draft.url) ? "" : __("That doesn't look like a web address.") @@ -49,15 +35,10 @@ const labelError = computed(() => draft.label.trim() ? "" : __("Give the link a name attendees will recognise."), ) -function chooseIcon(icon: string) { - iconChosen = true - draft.icon = icon -} - function submit() { showErrors.value = true if (urlError.value || labelError.value) return - emit("submit", { icon: draft.icon, label: draft.label.trim(), url: normalizeUrl(draft.url) }) + emit("submit", { icon: draft.icon, label: draft.label.trim(), url: scheme + draft.url.trim() }) isOpen.value = false } @@ -101,7 +82,7 @@ function remove() { :title="__(option.label)" :icon="option.icon" :variant="draft.icon === option.value ? 'subtle' : 'ghost'" - @click="chooseIcon(option.value)" + @click="draft.icon = option.value" /> diff --git a/dashboard/src/pages/manage/events/EventDetails.vue b/dashboard/src/pages/manage/events/EventDetails.vue index 18c133b3..5c58150d 100644 --- a/dashboard/src/pages/manage/events/EventDetails.vue +++ b/dashboard/src/pages/manage/events/EventDetails.vue @@ -81,7 +81,7 @@ function fill(detail: EventDetail) { time_zone: detail.time_zone ?? "", venue: detail.venue?.name ?? "", meeting_link: detail.meeting_link ?? "", - external_links: detail.external_links.map(({ icon, label, url }) => ({ icon, label, url })), + external_links: detail.external_links.map((link) => ({ ...link })), }) // The editor rewrites its own HTML once it mounts, so the baseline is taken after // that settles — otherwise the page loads already dirty. diff --git a/dashboard/src/utils/eventLinks.test.ts b/dashboard/src/utils/eventLinks.test.ts index 0cf1f17f..8dce0b02 100644 --- a/dashboard/src/utils/eventLinks.test.ts +++ b/dashboard/src/utils/eventLinks.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict" import { test } from "node:test" -import { hostOf, isValidUrl, linkIconClass, normalizeUrl, suggestLink } from "./eventLinks.ts" +import { hostOf, isValidUrl, linkIconClass, normalizeUrl } from "./eventLinks.ts" test("adds https to a bare address", () => { assert.equal(normalizeUrl(" lu.ma/pune "), "https://lu.ma/pune") @@ -20,25 +20,6 @@ test("shows the host without www", () => { assert.equal(hostOf("https://www.github.com/frappe"), "github.com") }) -test("suggests an icon and label from known services", () => { - assert.deepEqual(suggestLink("https://maps.app.goo.gl/abc"), { - icon: "map-pin", - label: "Venue on Google Maps", - }) - assert.deepEqual(suggestLink("t.me/frappepune"), { - icon: "message-circle", - label: "Community chat", - }) -}) - -test("falls back to the host for unknown services", () => { - assert.deepEqual(suggestLink("https://www.frappe.io/events"), { - icon: "globe", - label: "frappe.io", - }) - assert.equal(suggestLink("nope"), null) -}) - test("draws an unknown or missing icon as a link", () => { assert.equal(linkIconClass("map-pin"), "lucide-map-pin") assert.equal(linkIconClass(null), "lucide-link") diff --git a/dashboard/src/utils/eventLinks.ts b/dashboard/src/utils/eventLinks.ts index 65d36c62..06f010b7 100644 --- a/dashboard/src/utils/eventLinks.ts +++ b/dashboard/src/utils/eventLinks.ts @@ -15,20 +15,6 @@ export const LINK_ICONS = [ { value: "link", label: "Link", icon: "lucide-link" }, ] -const SUGGESTIONS: [RegExp, string, string][] = [ - [/github\.com|gitlab\.com/, "folder-git-2", "Repository"], - [/youtube\.com|youtu\.be|vimeo\.com/, "circle-play", "Watch the recording"], - [/linkedin\.com|x\.com|twitter\.com|instagram\.com/, "users", "Follow us"], - [/maps\.app\.goo\.gl|google\.[a-z.]+\/maps|goo\.gl\/maps/, "map-pin", "Venue on Google Maps"], - [/docs\.google\.com\/presentation|speakerdeck|slideshare/, "presentation", "Slides"], - [/docs\.google\.com\/forms|forms\.gle|typeform/, "clipboard-list", "Form"], - [/docs\.google|notion\.(so|site)/, "file-text", "Event notes"], - [/discord|t\.me|telegram|chat\.whatsapp|slack\.com/, "message-circle", "Community chat"], - [/zoom\.us|meet\.google|teams\.microsoft/, "video", "Join online"], - [/photos\.google|photos\.app\.goo\.gl|flickr/, "camera", "Event photos"], - [/lu\.ma|calendar/, "calendar", "Add to calendar"], -] - export function linkIconClass(icon: string | null) { return LINK_ICONS.find((option) => option.value === icon)?.icon ?? "lucide-link" } @@ -55,10 +41,3 @@ export function hostOf(url: string) { return url } } - -export function suggestLink(raw: string): { icon: string; label: string } | null { - if (!isValidUrl(raw)) return null - const url = normalizeUrl(raw) - const match = SUGGESTIONS.find(([pattern]) => pattern.test(url)) - return match ? { icon: match[1], label: match[2] } : { icon: "globe", label: hostOf(url) } -} diff --git a/e2e/tests/manage-event.spec.ts b/e2e/tests/manage-event.spec.ts index 07827309..034adcaa 100644 --- a/e2e/tests/manage-event.spec.ts +++ b/e2e/tests/manage-event.spec.ts @@ -435,7 +435,8 @@ test.describe("Event links", () => { const dialog = page.getByRole("dialog") await dialog.getByRole("textbox", { name: "URL" }).fill("t.me/buzz-e2e") - await expect(dialog.getByRole("textbox", { name: "Label" })).toHaveValue("Community chat") + await dialog.getByRole("textbox", { name: "Label" }).fill("Community chat") + await dialog.getByRole("button", { name: "Community", exact: true }).click() await dialog.getByRole("button", { name: "Add", exact: true }).click() await expect(dialog).toBeHidden()