feat: P2.9 VNCtalk + P2.10 Collabora + P2.11 Calendar Enhancements + P2.13 VNCdirectory Admin

- P2.9: VNCtalk video meeting — create/update meeting from event modal,
  'Join Meeting' link in event detail. Admin config vnctalkServerUrl.
- P2.10: Collabora online editing — 'Edit with Collabora' for office files,
  WOPI discovery + edit URL. Admin config collaboraServerUrl.
- P2.11: Calendar enhancements — clickable links in descriptions,
  participant contact popover, Reply/Reply All from event, timezone picker,
  map links for locations.
- P2.13: VNCdirectory IDP admin panel — Connection, SAML/IDP, LDAP,
  Authentication, Federated Apps configuration. Secret masking on display.
This commit is contained in:
Bernd Rodler
2026-08-07 13:38:12 +02:00
parent e7acf56753
commit 13ec05da83
16 changed files with 1512 additions and 34 deletions
+62 -1
View File
@@ -11,7 +11,7 @@ import {
AlertCircle, Star, Clock, FolderUp,
FileArchive, FileSpreadsheet, Presentation, FileCode,
Box, PenTool, Terminal as TerminalIcon, Database, Type as TypeIcon,
Menu, Users, Share2, MailPlus, Paperclip,
Menu, Users, Share2, MailPlus, Paperclip, ExternalLink,
} from "lucide-react";
import { useIsDesktop } from "@/hooks/use-media-query";
import { Button } from "@/components/ui/button";
@@ -208,6 +208,14 @@ function isDatabaseFile(name: string): boolean {
return DATABASE_EXTENSIONS.has(ext);
}
const OFFICE_EXTENSIONS = new Set([
"docx", "xlsx", "pptx", "odt", "ods", "odp", "doc", "xls", "ppt",
]);
function isOfficeFile(name: string): boolean {
const ext = name.split(".").pop()?.toLowerCase() || "";
return OFFICE_EXTENSIONS.has(ext);
}
function isPreviewable(name: string): boolean {
return isImageFile(name) || isTextFile(name) || isPdfFile(name) || isAudioFile(name) || isVideoFile(name);
}
@@ -1075,6 +1083,33 @@ export function FileBrowser({
{t("send_as_attachment")} {fileNames.length > 1 && `(${fileNames.length})`}
</Button>
)}
{!showBatch && hasFiles && fileNames.length === 1 && isOfficeFile(fileNames[0]) && (
<Button
variant="ghost"
size="sm"
className="h-8"
onClick={async () => {
const file = resources.find((r) => r.name === fileNames[0]);
if (!file) return;
try {
const res = await fetch("/api/collabora/edit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileId: file.id, fileName: file.name }),
});
if (res.ok) {
const { url } = await res.json();
window.open(url, "_blank", "noopener,noreferrer");
}
} catch (err) {
console.error("Collabora edit failed:", err);
}
}}
>
<Pencil className="w-4 h-4 me-1" />
Edit with Collabora
</Button>
)}
</>
);
})()}
@@ -1797,6 +1832,32 @@ export function FileBrowser({
{t("download")}
</button>
)}
{!resources.find(r => r.name === contextMenu.name)?.isDirectory && isOfficeFile(contextMenu.name) && (
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={async () => {
const file = resources.find((r) => r.name === contextMenu.name);
if (!file) { setContextMenu(null); return; }
try {
const res = await fetch("/api/collabora/edit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileId: file.id, fileName: file.name }),
});
if (res.ok) {
const { url } = await res.json();
window.open(url, "_blank", "noopener,noreferrer");
}
} catch (err) {
console.error("Collabora edit failed:", err);
}
setContextMenu(null);
}}
>
<ExternalLink className="w-4 h-4" />
Edit with Collabora
</button>
)}
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted transition-colors text-start"
onClick={() => {