ultisuite-client/lib/drive/drive-filters.ts
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- Updated .env.example to include configuration for OnlyOffice Document Server.
- Modified the workspace configuration to remove the drive-suite path.
- Adjusted TypeScript environment imports for consistency.
- Enhanced Next.js configuration to disable canvas in Webpack.
- Updated package.json to include new dependencies for OnlyOffice and PDF.js.
- Added global styles for OnlyOffice theme integration in the CSS.
- Created new layout and page components for the Drive feature, including public sharing and editing functionalities.
- Updated metadata handling across various layouts to reflect the new app structure.
2026-06-07 15:49:21 +02:00

233 lines
6.9 KiB
TypeScript

import type { DriveFileInfo } from "@/lib/api/types"
import { parentFolderPath } from "@/lib/drive/drive-search"
import { normalizeDriveFolderPath } from "@/lib/drive/drive-sidebar-tree"
import type { DriveFiltersSnapshot } from "@/lib/stores/drive-filters-store"
import { driveFiltersActive } from "@/lib/stores/drive-filters-store"
export type DriveMimeCategory =
| "folder"
| "document"
| "spreadsheet"
| "presentation"
| "image"
| "pdf"
| "video"
| "audio"
| "archive"
| "other"
export type DriveSourceId = "ultimail" | "ultimeet"
const ARCHIVE_EXT = new Set(["zip", "rar", "7z", "tar", "gz", "bz2"])
const AUDIO_EXT = new Set(["mp3", "wav", "ogg", "flac", "m4a", "aac"])
const IMAGE_EXT = new Set(["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "avif", "heic"])
const VIDEO_EXT = new Set(["mp4", "webm", "mov", "mkv", "ogv", "m4v"])
function ext(name: string) {
const i = name.lastIndexOf(".")
return i > 0 ? name.slice(i + 1).toLowerCase() : ""
}
export function classifyDriveFile(file: DriveFileInfo): DriveMimeCategory {
if (file.type === "directory") return "folder"
const mime = (file.mime_type ?? "").toLowerCase()
const e = ext(file.name)
if (mime.includes("pdf") || e === "pdf") return "pdf"
if (mime.startsWith("image/") || IMAGE_EXT.has(e)) return "image"
if (mime.startsWith("video/") || VIDEO_EXT.has(e)) return "video"
if (mime.startsWith("audio/") || AUDIO_EXT.has(e)) return "audio"
if (
mime.includes("zip") ||
mime.includes("archive") ||
ARCHIVE_EXT.has(e)
) {
return "archive"
}
if (
mime.includes("spreadsheet") ||
mime.includes("excel") ||
["xls", "xlsx", "ods", "csv"].includes(e)
) {
return "spreadsheet"
}
if (
mime.includes("presentation") ||
mime.includes("powerpoint") ||
["ppt", "pptx", "odp"].includes(e)
) {
return "presentation"
}
if (
mime.includes("word") ||
mime.includes("document") ||
mime.includes("opendocument.text") ||
["doc", "docx", "odt", "rtf", "txt"].includes(e)
) {
return "document"
}
return "other"
}
export function matchesDriveSource(file: DriveFileInfo, source: DriveSourceId): boolean {
if (file.source === source) return true
const p = file.path.toLowerCase()
const n = file.name.toLowerCase()
if (source === "ultimail") {
return (
p.includes("/ultimail") ||
p.includes("/mail/") ||
n.includes("ultimail") ||
(file.mime_type ?? "").includes("rfc822")
)
}
return (
file.source === "ultimeet" ||
p.includes("/ultimeet") ||
p.includes("/meet") ||
p.includes("/recordings") ||
n.includes("ultimeet") ||
n.includes("meet-")
)
}
function parseModified(iso: string): Date | null {
const d = new Date(iso)
return Number.isNaN(d.getTime()) ? null : d
}
function inDateRange(file: DriveFileInfo, filters: DriveFiltersSnapshot): boolean {
const { datePreset, dateFrom, dateTo } = filters
if (!datePreset) return true
const modified = parseModified(file.last_modified)
if (!modified) return true
const now = new Date()
const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate())
if (datePreset === "today") {
const today = startOfDay(now)
return modified >= today
}
if (datePreset === "last7") {
const from = new Date(now)
from.setDate(from.getDate() - 7)
return modified >= from
}
if (datePreset === "last30") {
const from = new Date(now)
from.setDate(from.getDate() - 30)
return modified >= from
}
if (datePreset === "thisYear") {
return modified.getFullYear() === now.getFullYear()
}
if (datePreset === "lastYear") {
return modified.getFullYear() === now.getFullYear() - 1
}
if (datePreset === "custom" && dateFrom) {
const from = startOfDay(new Date(dateFrom))
const to = dateTo ? new Date(dateTo) : now
return modified >= from && modified <= to
}
return true
}
export function matchesDriveFilters(
file: DriveFileInfo,
filters: DriveFiltersSnapshot
): boolean {
if (filters.types.size > 0) {
const cat = classifyDriveFile(file)
if (!filters.types.has(cat)) return false
}
if (filters.sources.size > 0) {
const ok = [...filters.sources].some((s) => matchesDriveSource(file, s))
if (!ok) return false
}
if (!matchesContact(file, filters)) return false
if (!inDateRange(file, filters)) return false
return true
}
function isPathUnderScope(path: string, scopePath: string): boolean {
const normalized = normalizeDriveFolderPath(path)
const scope = normalizeDriveFolderPath(scopePath)
if (scope === "/") return true
return normalized === scope || normalized.startsWith(`${scope}/`)
}
/** Folder paths that contain (or nest) at least one file matching filters. */
export function buildDriveFolderPathsWithMatches(
corpus: DriveFileInfo[],
filters: DriveFiltersSnapshot,
scopePath = "/"
): Set<string> {
const keep = new Set<string>()
const scope = normalizeDriveFolderPath(scopePath)
for (const file of corpus) {
if (file.type === "directory") continue
if (!matchesDriveFilters(file, filters)) continue
let dir = parentFolderPath(file.path)
while (dir !== "/" && isPathUnderScope(dir, scope)) {
const normalized = normalizeDriveFolderPath(dir)
keep.add(normalized)
if (normalized === scope) break
dir = parentFolderPath(normalized)
}
}
return keep
}
function matchesContact(file: DriveFileInfo, filters: DriveFiltersSnapshot): boolean {
if (!filters.contactEmail && !filters.contactName) return true
const hay = `${file.path} ${file.name}`.toLowerCase()
if (filters.contactEmail && hay.includes(filters.contactEmail.toLowerCase())) {
return true
}
if (filters.contactName) {
const parts = filters.contactName.toLowerCase().split(/\s+/).filter(Boolean)
if (parts.length > 0 && parts.every((p) => hay.includes(p))) return true
}
return false
}
export function applyDriveFilters(
items: DriveFileInfo[],
filters: DriveFiltersSnapshot,
options?: {
/** When set, only folders in this set stay visible (plus direct filter matches). */
folderKeepPaths?: Set<string>
/** Corpus for folder pruning when folderKeepPaths is omitted. */
matchCorpus?: DriveFileInfo[]
/** Limit folder pruning to descendants of this path (default `/`). */
scopePath?: string
}
): DriveFileInfo[] {
if (!driveFiltersActive(filters)) return items
const folderKeepPaths =
options?.folderKeepPaths ??
(options?.matchCorpus
? buildDriveFolderPathsWithMatches(
options.matchCorpus,
filters,
options.scopePath ?? "/"
)
: undefined)
return items.filter((file) => {
if (file.type === "directory") {
const path = normalizeDriveFolderPath(file.path)
if (folderKeepPaths?.has(path)) return true
return matchesDriveFilters(file, filters)
}
return matchesDriveFilters(file, filters)
})
}
export { driveFiltersActive as hasActiveDriveFilters }