Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion buzz/events/doctype/buzz_event/buzz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 7 additions & 26 deletions dashboard/src/components/dashboard/events/EventLinkDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,28 @@ 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<boolean>({ required: true })
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.")
Expand All @@ -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
}

Expand Down Expand Up @@ -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"
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/pages/manage/events/EventDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 1 addition & 20 deletions dashboard/src/utils/eventLinks.test.ts
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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")
Expand Down
21 changes: 0 additions & 21 deletions dashboard/src/utils/eventLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -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) }
}
3 changes: 2 additions & 1 deletion e2e/tests/manage-event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading