ultisuite-client/lib/drive/drive-url.ts
R3D347HR4Y ad1370ea7e
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat: enhance configuration and add new demo layouts
- Introduced turbopack alias for canvas in next.config.mjs.
- Updated package.json scripts for development and branding tasks.
- Added new dependencies for Tiptap extensions.
- Implemented new demo layouts for agenda, contacts, drive, and mail applications.
- Enhanced globals.css for improved theming and splash screen animations.
- Added OAuth callback handling for drive mounts.
- Updated layout components to integrate new demo shells and improve structure.
2026-06-12 19:10:24 +02:00

281 lines
9.1 KiB
TypeScript

export type DriveRootKind = "personal" | "org" | "mount" | "shared"
export type DriveView =
| "files"
| "shared"
| "org"
| "mount"
| "recent"
| "starred"
| "trash"
| "search"
/** 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 driveRouteBase(routeRoot = "drive"): string {
return `/${routeRoot}`
}
export function buildDriveFolderHref(
view: Extract<DriveView, "files" | "shared" | "org" | "mount">,
segments: string[],
rootId?: string,
routeRoot = "drive"
): string {
const base = driveRouteBase(routeRoot)
const decoded = decodePathSegments(segments)
if (view === "org" && rootId) {
if (decoded.length === 0) return `${base}/org/${encodeURIComponent(rootId)}`
return `${base}/org/${encodeURIComponent(rootId)}/folders/${encodePathSegments(decoded)}`
}
if (view === "mount" && rootId) {
if (decoded.length === 0) return `${base}/mounts/${encodeURIComponent(rootId)}`
return `${base}/mounts/${encodeURIComponent(rootId)}/folders/${encodePathSegments(decoded)}`
}
if (decoded.length === 0) {
return view === "shared" ? `${base}/shared` : base
}
const prefix = view === "shared" ? `${base}/shared/folders` : `${base}/folders`
return `${prefix}/${encodePathSegments(decoded)}`
}
export interface DriveRouteState {
view: DriveView
pathSegments: string[]
page: number
fileId: string | null
query: string
rootId: string | null
}
export function parseDriveSegments(segments: string[] | undefined): DriveRouteState {
const parts = segments ?? []
if (parts.length === 0) {
return { view: "files", pathSegments: [], page: 1, fileId: null, query: "", rootId: null }
}
const head = parts[0]
if (head === "recent" || head === "starred" || head === "trash") {
return { view: head, pathSegments: [], page: 1, fileId: null, query: "", rootId: null }
}
if (head === "org" && parts[1]) {
const folderId = decodePathSegment(parts[1])
const folderParts = parts.slice(2)
if (folderParts[0] === "folders") {
folderParts.shift()
const pageIdx = folderParts.indexOf("page")
let page = 1
if (pageIdx >= 0 && folderParts[pageIdx + 1]) {
page = Math.max(1, parseInt(folderParts[pageIdx + 1], 10) || 1)
folderParts.splice(pageIdx, 2)
}
return {
view: "org",
pathSegments: decodePathSegments(folderParts),
page,
fileId: null,
query: "",
rootId: folderId,
}
}
return { view: "org", pathSegments: [], page: 1, fileId: null, query: "", rootId: folderId }
}
if (head === "mounts" && parts[1]) {
const mountId = decodePathSegment(parts[1])
const folderParts = parts.slice(2)
if (folderParts[0] === "folders") {
folderParts.shift()
const pageIdx = folderParts.indexOf("page")
let page = 1
if (pageIdx >= 0 && folderParts[pageIdx + 1]) {
page = Math.max(1, parseInt(folderParts[pageIdx + 1], 10) || 1)
folderParts.splice(pageIdx, 2)
}
return {
view: "mount",
pathSegments: decodePathSegments(folderParts),
page,
fileId: null,
query: "",
rootId: mountId,
}
}
return { view: "mount", pathSegments: [], page: 1, fileId: null, query: "", rootId: mountId }
}
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: "",
rootId: null,
}
}
return { view: "shared", pathSegments: [], page: 1, fileId: null, query: "", rootId: null }
}
if (head === "search") {
return { view: "search", pathSegments: [], page: 1, fileId: null, query: "", rootId: null }
}
if (head === "edit" && parts[1]) {
return {
view: "files",
pathSegments: [],
page: 1,
fileId: decodeURIComponent(parts[1]),
query: "",
rootId: null,
}
}
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: "",
rootId: null,
}
}
return {
view: "files",
pathSegments: decodePathSegments(parts),
page: 1,
fileId: null,
query: "",
rootId: null,
}
}
export function buildDrivePath(state: DriveRouteState, routeRoot = "drive"): string {
const base = driveRouteBase(routeRoot)
if (state.view === "recent") return `${base}/recent`
if (state.view === "starred") return `${base}/starred`
if (state.view === "trash") return `${base}/trash`
if (state.view === "search") return `${base}/search`
if (state.view === "org" && state.rootId) {
const folder = state.pathSegments.length
? `${base}/org/${encodeURIComponent(state.rootId)}/folders/${encodePathSegments(state.pathSegments)}`
: `${base}/org/${encodeURIComponent(state.rootId)}`
const pageSuffix = state.page > 1 ? `/page/${state.page}` : ""
return `${folder}${pageSuffix}`
}
if (state.view === "mount" && state.rootId) {
const folder = state.pathSegments.length
? `${base}/mounts/${encodeURIComponent(state.rootId)}/folders/${encodePathSegments(state.pathSegments)}`
: `${base}/mounts/${encodeURIComponent(state.rootId)}`
const pageSuffix = state.page > 1 ? `/page/${state.page}` : ""
return `${folder}${pageSuffix}`
}
if (state.view === "shared") {
const folder = state.pathSegments.length
? `${base}/shared/folders/${encodePathSegments(state.pathSegments)}`
: `${base}/shared`
const pageSuffix = state.page > 1 ? `/page/${state.page}` : ""
return `${folder}${pageSuffix}`
}
if (state.fileId && state.view === "files") {
return `${base}/edit/${encodeURIComponent(state.fileId)}`
}
const folder = state.pathSegments.length
? `${base}/folders/${encodePathSegments(state.pathSegments)}`
: base
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") && !path.startsWith("/demo/drive")) return false
if (path.startsWith("//")) return false
if (path.includes("://")) return false
return true
}
/** Stable Nextcloud file id segment (numeric). */
export function isDriveFileIdSegment(value: string): boolean {
return /^[1-9][0-9]*$/.test(value)
}
/** Rich-text editor URL — id-only, no returnTo query param. */
export function buildDriveDocsEditHref(fileId: string | number): string {
return `/drive/docs/${String(fileId)}/edit`
}
/** UltiDraw (Excalidraw) editor URL — id-only. */
export function buildDriveDrawEditHref(fileId: string | number): string {
return `/drive/draw/${String(fileId)}/edit`
}
export function buildDriveEditHref(filePath: string, returnTo?: string): string {
const params = new URLSearchParams()
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 session returnTo, else explicit returnTo, else parent folder. */
export function resolveDriveEditReturnTo(
returnTo: string | null | undefined,
filePath: string,
folderHref: (folderPath: string) => string,
sessionReturnTo?: string | null
): string {
if (sessionReturnTo && isSafeDriveReturnPath(sessionReturnTo)) return sessionReturnTo
if (returnTo && isSafeDriveReturnPath(returnTo)) return returnTo
return folderHref(parentFolderPathFromFilePath(filePath))
}
export function driveRootKindForView(view: DriveView): DriveRootKind {
if (view === "org") return "org"
if (view === "mount") return "mount"
if (view === "shared") return "shared"
return "personal"
}