100 lines
2.9 KiB
TypeScript
100 lines
2.9 KiB
TypeScript
|
|
/**
|
||
|
|
* Workspace link utilities.
|
||
|
|
*
|
||
|
|
* All workspace links use REAL URLs so they work if the browser follows them:
|
||
|
|
* Files/docs: /workspace?path=knowledge/path/to/doc.md
|
||
|
|
* Objects: /workspace?path=knowledge/leads
|
||
|
|
* Entries: /workspace?entry=leads:abc123
|
||
|
|
*/
|
||
|
|
|
||
|
|
export type WorkspaceLink =
|
||
|
|
| { kind: "file"; path: string }
|
||
|
|
| { kind: "entry"; objectName: string; entryId: string };
|
||
|
|
|
||
|
|
// --- Builders ---
|
||
|
|
|
||
|
|
/** Build a real URL for an entry detail modal. */
|
||
|
|
export function buildEntryLink(objectName: string, entryId: string): string {
|
||
|
|
return `/workspace?entry=${encodeURIComponent(objectName)}:${encodeURIComponent(entryId)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Build a real URL for a file or object in the workspace. */
|
||
|
|
export function buildFileLink(path: string): string {
|
||
|
|
return `/workspace?path=${encodeURIComponent(path)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Parsers ---
|
||
|
|
|
||
|
|
/** Parse a workspace URL into a structured link. Returns null if not a workspace link. */
|
||
|
|
export function parseWorkspaceLink(href: string): WorkspaceLink | null {
|
||
|
|
// Handle full or relative /workspace?... URLs
|
||
|
|
let url: URL | null = null;
|
||
|
|
try {
|
||
|
|
if (href.startsWith("/workspace")) {
|
||
|
|
url = new URL(href, "http://localhost");
|
||
|
|
} else if (href.includes("/workspace?")) {
|
||
|
|
url = new URL(href);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// not a valid URL
|
||
|
|
}
|
||
|
|
|
||
|
|
if (url) {
|
||
|
|
const entryParam = url.searchParams.get("entry");
|
||
|
|
if (entryParam && entryParam.includes(":")) {
|
||
|
|
const colonIdx = entryParam.indexOf(":");
|
||
|
|
return {
|
||
|
|
kind: "entry",
|
||
|
|
objectName: entryParam.slice(0, colonIdx),
|
||
|
|
entryId: entryParam.slice(colonIdx + 1),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const pathParam = url.searchParams.get("path");
|
||
|
|
if (pathParam) {
|
||
|
|
return { kind: "file", path: pathParam };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Legacy: handle old @entry/ format for backward compat
|
||
|
|
if (href.startsWith("@entry/")) {
|
||
|
|
const rest = href.slice("@entry/".length);
|
||
|
|
const slashIdx = rest.indexOf("/");
|
||
|
|
if (slashIdx > 0) {
|
||
|
|
return {
|
||
|
|
kind: "entry",
|
||
|
|
objectName: rest.slice(0, slashIdx),
|
||
|
|
entryId: rest.slice(slashIdx + 1),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Check if an href is a workspace link (either /workspace?... or legacy @entry/). */
|
||
|
|
export function isWorkspaceLink(href: string): boolean {
|
||
|
|
return (
|
||
|
|
href.startsWith("/workspace?") ||
|
||
|
|
href.startsWith("/workspace#") ||
|
||
|
|
href === "/workspace" ||
|
||
|
|
href.startsWith("@entry/")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Check if an href is a workspace-internal link (not external URL). */
|
||
|
|
export function isInternalLink(href: string): boolean {
|
||
|
|
return (
|
||
|
|
!href.startsWith("http://") &&
|
||
|
|
!href.startsWith("https://") &&
|
||
|
|
!href.startsWith("mailto:")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Check if an href is an entry link (any format). */
|
||
|
|
export function isEntryLink(href: string): boolean {
|
||
|
|
if (href.startsWith("@entry/")) {return true;}
|
||
|
|
if (href.startsWith("/workspace") && href.includes("entry=")) {return true;}
|
||
|
|
return false;
|
||
|
|
}
|