From e34122965e9e2bc2e2c9538f294c361c48ee541e Mon Sep 17 00:00:00 2001 From: Andreas Molnar Date: Thu, 3 Sep 2026 23:59:18 +0200 Subject: [PATCH 01/11] feat(dashboard): add link routing --- apps/backend/src/jobs/shortcuts-indexer.ts | 4 +- apps/backend/src/lib/data/links.ts | 29 +++++++ .../components/settings/LinkDetailsForm.tsx | 81 ++++++++++++++++++- .../web/src/components/widgets/CommandBar.tsx | 20 ++++- apps/web/src/components/widgets/LinkView.tsx | 35 +++++++- .../components/widgets/ShortcutsWidget.tsx | 16 ++-- packages/types/package.json | 4 + packages/types/sdk-types.ts | 7 ++ 8 files changed, 183 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/jobs/shortcuts-indexer.ts b/apps/backend/src/jobs/shortcuts-indexer.ts index 8132041d..b28af1fe 100644 --- a/apps/backend/src/jobs/shortcuts-indexer.ts +++ b/apps/backend/src/jobs/shortcuts-indexer.ts @@ -55,7 +55,7 @@ export async function runShortcutsIndexing() { name, icon: String(link?.iconUrl || link?.folderIcon || "/icons/faGlobe.svg"), secondary: String(link?.collection || link?.folder || "Link"), - action: url.startsWith("url:") ? url : `url:${url}`, + action: "link", app: "", tags: [ name, @@ -381,7 +381,7 @@ async function rebuildUserShortcuts(pb: any, userId: string, rows: ShortcutRow[] const existingRecords = existingBySource.get(sid) || []; // Determine if it's a link or integration - const isLink = newRows.length === 1 && newRows[0].app === "" && newRows[0].action.startsWith("url:"); + const isLink = newRows.length === 1 && newRows[0].app === "" && (newRows[0].action === "link" || newRows[0].action.startsWith("url:")); if (isLink) { const newRow = newRows[0]; diff --git a/apps/backend/src/lib/data/links.ts b/apps/backend/src/lib/data/links.ts index e9502fe8..0eebc05b 100644 --- a/apps/backend/src/lib/data/links.ts +++ b/apps/backend/src/lib/data/links.ts @@ -45,11 +45,18 @@ export interface HomeLinkFolderPathItem { parentFolder?: string; } +export interface SecondaryUrl { + name: string; + url: string; + routingRule: string; +} + export interface LinkItem { collectionId: string; collectionName: "linkItems"; id: string; url: string; + secondaryUrls?: SecondaryUrl[]; title: string; iconUrl: string; description: string; @@ -185,6 +192,7 @@ export async function getLinksItems(userId: string, listId: string, folderId?: s return records.map((r) => ({ id: r.id, url: r.url, + secondaryUrls: normalizeSecondaryUrls((r as any).secondaryUrls), title: r.title, iconUrl: r.iconUrl, description: r.description, @@ -298,6 +306,15 @@ function buildFolderPathResolver( }; } +function normalizeSecondaryUrls(raw: unknown): SecondaryUrl[] { + if (!Array.isArray(raw)) return []; + return raw.map((entry: any) => ({ + name: String(entry?.name ?? "").trim(), + url: String(entry?.url ?? "").trim(), + routingRule: String(entry?.routingRule ?? "").trim(), + })).filter((entry) => entry.url && entry.routingRule); +} + function getMonitorLinkId(monitor: { sourcelinkId?: string; linkId?: string }) { return String(monitor.sourcelinkId || monitor.linkId || "").trim(); } @@ -454,6 +471,7 @@ export async function getHomeLinks(userId: string) { return { id: r.id as string, url: r.url as string, + secondaryUrls: normalizeSecondaryUrls((r as any).secondaryUrls), title: r.title as string, iconUrl: r.iconUrl as string, description: r.description as string, @@ -608,11 +626,13 @@ export async function createHomeLinkItem( linkGroup?: string; folder?: string; statusCheck?: boolean; + secondaryUrls?: SecondaryUrl[]; }, ): Promise< { id: string; url: string; + secondaryUrls?: SecondaryUrl[]; title: string; iconUrl: string; description: string; @@ -663,6 +683,7 @@ export async function createHomeLinkItem( collection: homeCollection.id, folder: folderId ?? "", position: nextPosition, + secondaryUrls: normalizeSecondaryUrls(data.secondaryUrls), }); await syncHomeLinkMonitor(userId, record.id, record.url, data.statusCheck); @@ -670,6 +691,7 @@ export async function createHomeLinkItem( return { id: record.id, url: record.url, + secondaryUrls: normalizeSecondaryUrls((record as any).secondaryUrls), title: record.title, iconUrl: record.iconUrl, description: record.description, @@ -766,6 +788,7 @@ export async function updateHomeLinkItem( linkGroup?: string; folder?: string; statusCheck?: boolean; + secondaryUrls?: SecondaryUrl[]; }, ): Promise { const pb = getServerPB(); @@ -781,6 +804,7 @@ export async function updateHomeLinkItem( if (data.title !== undefined) updateData.title = data.title; if (data.iconUrl !== undefined) updateData.iconUrl = data.iconUrl; if (data.description !== undefined) updateData.description = data.description; + if (data.secondaryUrls !== undefined) updateData.secondaryUrls = normalizeSecondaryUrls(data.secondaryUrls); if (data.linkGroup !== undefined || data.folder !== undefined) { const homeListId = await getHomeListId(userId); @@ -954,9 +978,11 @@ export async function createLinkItem(data: { collection: string; folder?: string; tags?: string[]; + secondaryUrls?: SecondaryUrl[]; }): Promise<{ id: string; url: string; + secondaryUrls?: SecondaryUrl[]; title: string; iconUrl: string; description: string; @@ -975,11 +1001,13 @@ export async function createLinkItem(data: { collection: data.collection, folder: data.folder ?? "", tags: Array.isArray(data.tags) ? data.tags : [], + secondaryUrls: normalizeSecondaryUrls(data.secondaryUrls), }); return { id: record.id, url: record.url, + secondaryUrls: normalizeSecondaryUrls((record as any).secondaryUrls), title: record.title, iconUrl: record.iconUrl, description: record.description, @@ -1000,6 +1028,7 @@ export async function createCollectionLinkItem( collection: string; folder?: string; tags?: string[]; + secondaryUrls?: SecondaryUrl[]; }, ): Promise<{ id: string; diff --git a/apps/web/src/components/settings/LinkDetailsForm.tsx b/apps/web/src/components/settings/LinkDetailsForm.tsx index 63bbc550..d5524a9b 100644 --- a/apps/web/src/components/settings/LinkDetailsForm.tsx +++ b/apps/web/src/components/settings/LinkDetailsForm.tsx @@ -46,9 +46,16 @@ import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { Switch } from "@/components/ui/switch"; import { faEllipsisV, faPaperclip } from "@fortawesome/free-solid-svg-icons"; import { Icon as Iconify } from "@iconify-icon/react"; -import { Check, ChevronsUpDown } from "lucide-react"; +import { Check, ChevronsUpDown, CircleHelp, Plus, Trash2 } from "lucide-react"; import { cn } from "@/lib/utils"; -import type { LinkType, StatusCheckAuth, StatusCheckMethod } from "@dashwise/types/sdk"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import type { LinkType, StatusCheckAuth, StatusCheckMethod } from "@dashwise/types/links"; + +interface SecondaryUrl { + name: string; + url: string; + routingRule: string; +} interface Icon { Name: string; @@ -94,6 +101,8 @@ export default function LinkDetailsForm({ const [name, setName] = useState(""); const [linkId, setLinkId] = useState(() => link?.id || generateRandomId()); const [url, setUrl] = useState(""); + const [secondaryUrls, setSecondaryUrls] = useState([]); + const [secondaryUrlsExpanded, setSecondaryUrlsExpanded] = useState(false); const [icon, setIcon] = useState(null); const [linkGroup, setLinkGroup] = useState(() => preselectOpenedGroup || link?.linkGroup || link?.collection || ""); const [folder, setFolder] = useState(() => link?.folder || ""); @@ -137,6 +146,13 @@ export default function LinkDetailsForm({ if (link?.linkGroup) { setLinkGroup(link.linkGroup); } + if (Array.isArray((link as any)?.secondaryUrls)) { + setSecondaryUrls((link as any).secondaryUrls); + setSecondaryUrlsExpanded((link as any).secondaryUrls.length > 0); + } else { + setSecondaryUrls([]); + setSecondaryUrlsExpanded(false); + } if ((link as any)?.folder) { setFolder((link as any).folder); } @@ -252,6 +268,7 @@ export default function LinkDetailsForm({ linkGroup, folder, statusCheck, + secondaryUrls: secondaryUrls.filter((entry) => entry.url.trim() && entry.routingRule.trim()), }; if (isEditing && link?.id) { @@ -299,6 +316,8 @@ export default function LinkDetailsForm({ const resetForm = () => { setName(""); setUrl(""); + setSecondaryUrls([]); + setSecondaryUrlsExpanded(false); setIcon(null); setIconEdited(false); setFolder(""); @@ -422,6 +441,64 @@ export default function LinkDetailsForm({ onBlur={handleUrlBlur} /> +
+ {!secondaryUrlsExpanded ? ( + + ) : ( + <> + +
+
+
+ + + + + + Route this link to another URL when opened from a matching origin or hostname. + +
+ +
+ +
+ {secondaryUrls.map((entry, index) => ( +
+
+ setSecondaryUrls((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, name: e.target.value } : item))} className="frosted" /> + +
+ setSecondaryUrls((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, url: e.target.value } : item))} className="frosted" /> + setSecondaryUrls((current) => current.map((item, itemIndex) => itemIndex === index ? { ...item, routingRule: e.target.value } : item))} className="frosted" /> +
+ ))} +
+
+ + + )} +
+