ultisuite-client/lib/drive/drive-url.ts
R3D347HR4Y cdff12490a
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
hocuspocus
2026-06-09 14:31:07 +02:00

173 lines
5.3 KiB
TypeScript

export type DriveView = "files" | "recent" | "starred" | "trash" | "search" | "shared"
/** Decode one URL path segment (safe for names with literal `%`). */
export function decodePathSegment(segment: string): string {
try {
return decodeURIComponent(segment.replace(/\+/g, " "))
} catch {
return segment
}
}
function decodePathSegments(segments: string[]): string[] {
return segments.map(decodePathSegment)
}
export function encodePathSegments(segments: string[]): string {
return segments.map((s) => encodeURIComponent(decodePathSegment(s))).join("/")
}
export function buildDriveFolderHref(
view: Extract<DriveView, "files" | "shared">,
segments: string[]
): string {
const decoded = decodePathSegments(segments)
if (decoded.length === 0) {
return view === "shared" ? "/drive/shared" : "/drive"
}
const prefix = view === "shared" ? "/drive/shared/folders" : "/drive/folders"
return `${prefix}/${encodePathSegments(decoded)}`
}
export interface DriveRouteState {
view: DriveView
pathSegments: string[]
page: number
fileId: string | null
query: string
}
export function parseDriveSegments(segments: string[] | undefined): DriveRouteState {
const parts = segments ?? []
if (parts.length === 0) {
return { view: "files", pathSegments: [], page: 1, fileId: null, query: "" }
}
const head = parts[0]
if (head === "recent" || head === "starred" || head === "trash") {
return { view: head, pathSegments: [], page: 1, fileId: null, query: "" }
}
if (head === "shared") {
const folderParts = parts.slice(1)
let page = 1
if (folderParts[0] === "folders") {
folderParts.shift()
const pageIdx = folderParts.indexOf("page")
if (pageIdx >= 0 && folderParts[pageIdx + 1]) {
page = Math.max(1, parseInt(folderParts[pageIdx + 1], 10) || 1)
folderParts.splice(pageIdx, 2)
}
return {
view: "shared",
pathSegments: decodePathSegments(folderParts),
page,
fileId: null,
query: "",
}
}
return { view: "shared", pathSegments: [], page: 1, fileId: null, query: "" }
}
if (head === "search") {
return { view: "search", pathSegments: [], page: 1, fileId: null, query: "" }
}
if (head === "edit" && parts[1]) {
return {
view: "files",
pathSegments: [],
page: 1,
fileId: decodeURIComponent(parts[1]),
query: "",
}
}
if (head === "folders") {
const folderParts = parts.slice(1)
let page = 1
const pageIdx = folderParts.indexOf("page")
if (pageIdx >= 0 && folderParts[pageIdx + 1]) {
page = Math.max(1, parseInt(folderParts[pageIdx + 1], 10) || 1)
folderParts.splice(pageIdx, 2)
}
return {
view: "files",
pathSegments: decodePathSegments(folderParts),
page,
fileId: null,
query: "",
}
}
return {
view: "files",
pathSegments: decodePathSegments(parts),
page: 1,
fileId: null,
query: "",
}
}
export function buildDrivePath(state: DriveRouteState): string {
if (state.view === "recent") return "/drive/recent"
if (state.view === "starred") return "/drive/starred"
if (state.view === "trash") return "/drive/trash"
if (state.view === "search") return "/drive/search"
if (state.view === "shared") {
const folder = state.pathSegments.length
? `/drive/shared/folders/${encodePathSegments(state.pathSegments)}`
: "/drive/shared"
const pageSuffix = state.page > 1 ? `/page/${state.page}` : ""
return `${folder}${pageSuffix}`
}
if (state.fileId && state.view === "files") {
return `/drive/edit/${encodeURIComponent(state.fileId)}`
}
const folder = state.pathSegments.length
? `/drive/folders/${encodePathSegments(state.pathSegments)}`
: "/drive"
const pageSuffix = state.page > 1 ? `/page/${state.page}` : ""
return `${folder}${pageSuffix}`
}
export function folderPathFromSegments(segments: string[]): string {
const decoded = decodePathSegments(segments)
if (decoded.length === 0) return "/"
return "/" + decoded.join("/")
}
export function parentFolderPathFromFilePath(filePath: string): string {
const normalized = filePath.replace(/^\/+/, "").replace(/\/+$/, "")
if (!normalized) return "/"
const parts = normalized.split("/")
if (parts.length <= 1) return "/"
return "/" + parts.slice(0, -1).join("/")
}
function isSafeDriveReturnPath(path: string): boolean {
if (!path.startsWith("/drive")) return false
if (path.startsWith("//")) return false
if (path.includes("://")) return false
return true
}
export function buildDriveEditHref(
filePath: string,
returnTo?: string,
editor: "office" | "richtext" = "office"
): string {
const params = new URLSearchParams()
if (editor === "richtext") params.set("editor", "richtext")
if (returnTo && isSafeDriveReturnPath(returnTo)) {
params.set("returnTo", returnTo)
}
const base = `/drive/edit/${encodeURIComponent(filePath)}`
const qs = params.toString()
return qs ? `${base}?${qs}` : base
}
/** Resolve back link from editor: prefer explicit returnTo, else parent folder. */
export function resolveDriveEditReturnTo(
returnTo: string | null | undefined,
filePath: string,
folderHref: (folderPath: string) => string
): string {
if (returnTo && isSafeDriveReturnPath(returnTo)) return returnTo
return folderHref(parentFolderPathFromFilePath(filePath))
}