diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e7e5f..66b82a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T ## Unveröffentlicht +### Neu + +- Eigene Module: Als Kategorie steht jetzt auch „Eigene Module“ zur Auswahl. Einträge dort erscheinen gesammelt in einer eigenen Gruppe ganz unten in der Seitenleiste; die Gruppe ist nur zu sehen, solange ein Eintrag darin liegt. Bestehende Einträge verschieben Sie über „Bearbeiten“ dorthin. + ### Geändert - Seitenleiste: Ist sie eingeklappt, sind die Symbole etwas größer und stehen etwas enger beieinander. diff --git a/apps/api/src/custom-modules/dto/custom-module.dto.ts b/apps/api/src/custom-modules/dto/custom-module.dto.ts index a5a9b73..1c1960a 100644 --- a/apps/api/src/custom-modules/dto/custom-module.dto.ts +++ b/apps/api/src/custom-modules/dto/custom-module.dto.ts @@ -1,5 +1,5 @@ import { OmitType, PartialType } from '@nestjs/mapped-types'; -import { MODULE_CATEGORIES } from '@tessera/shared'; +import { CUSTOM_MODULE_CATEGORIES } from '@tessera/shared'; import { Transform } from 'class-transformer'; import { IsBoolean, @@ -60,8 +60,8 @@ export class CreateCustomModuleDto { @Validate(NurHttpsOhneZugangsdatenConstraint) url!: string; - @IsIn([...MODULE_CATEGORIES]) - category!: (typeof MODULE_CATEGORIES)[number]; + @IsIn([...CUSTOM_MODULE_CATEGORIES]) + category!: (typeof CUSTOM_MODULE_CATEGORIES)[number]; /** * quick-260929-dzu: `true` legt einen gemeinsamen Eintrag fuer alle Benutzer diff --git a/apps/web/src/components/custom-modules/custom-module-form-modal.tsx b/apps/web/src/components/custom-modules/custom-module-form-modal.tsx index 94b174b..f671264 100644 --- a/apps/web/src/components/custom-modules/custom-module-form-modal.tsx +++ b/apps/web/src/components/custom-modules/custom-module-form-modal.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useTranslations } from 'next-intl'; -import { MODULE_CATEGORIES } from '@tessera/shared'; +import { CUSTOM_MODULE_CATEGORIES } from '@tessera/shared'; import { type CustomModule, checkCustomModuleUrl, @@ -142,7 +142,7 @@ export function CustomModuleFormModal({ onChange={(e) => setCategory(e.target.value)} className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" > - {MODULE_CATEGORIES.map((c) => ( + {CUSTOM_MODULE_CATEGORIES.map((c) => ( diff --git a/apps/web/src/components/layout/sidebar.test.tsx b/apps/web/src/components/layout/sidebar.test.tsx index aed5b47..fe961f9 100644 --- a/apps/web/src/components/layout/sidebar.test.tsx +++ b/apps/web/src/components/layout/sidebar.test.tsx @@ -299,6 +299,32 @@ describe('Sidebar', () => { expect(screen.getByText('Domain-Tools')).toBeInTheDocument(); }); + it('Kategorie „Eigene Module“ steht zuletzt und nur, wenn ein Eintrag darin liegt', async () => { + mockCustomModules = [ + customModule({ id: 'cm-1', category: 'custom-modules', name: 'Mein Wiki' }), + customModule({ id: 'cm-2', category: 'fleet', name: 'Tankkarte' }), + ]; + const Sidebar = await importSidebar(); + const { unmount } = render(); + + await waitFor(() => { + expect(screen.getByText('Mein Wiki')).toBeInTheDocument(); + }); + const groups = Array.from( + screen.getByRole('group', { name: 'Kategorien' }).querySelectorAll(':scope > ul > li > button span.truncate'), + ).map((n) => n.textContent); + expect(groups.at(-1)).toBe('custom-modules'); + expect(groups).toContain('fleet'); + unmount(); + + mockCustomModules = [customModule({ category: 'fleet' })]; + render(); + await waitFor(() => { + expect(screen.getByText('fleet')).toBeInTheDocument(); + }); + expect(screen.queryByText('custom-modules')).toBeNull(); + }); + it('eingebaute Module stehen innerhalb einer Kategorie vor eigenen', async () => { mockCustomModules = [customModule({ category: 'Domain-Tools', name: 'Aaa Eigenes' })]; const Sidebar = await importSidebar(); diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx index c98d35f..cadb256 100644 --- a/apps/web/src/components/layout/sidebar.tsx +++ b/apps/web/src/components/layout/sidebar.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import { CUSTOM_MODULE_CATEGORY } from '@tessera/shared'; import { useTranslations } from 'next-intl'; import { TesseraLogo } from '@/components/brand/tessera-logo'; import { useSidebarStore } from '@/lib/stores/sidebar-store'; @@ -192,11 +193,15 @@ export function Sidebar() { categories.get(entry.category)!.push(entry); } - if (!searchQuery) return Array.from(categories.entries()); + // „Eigene Module“ steht immer zuletzt, die uebrigen in Fundreihenfolge. + const ordered = Array.from(categories.entries()).sort( + ([a], [b]) => Number(a === CUSTOM_MODULE_CATEGORY) - Number(b === CUSTOM_MODULE_CATEGORY), + ); + if (!searchQuery) return ordered; const q = searchQuery.toLowerCase(); const result: [string, SidebarEntry[]][] = []; - for (const [cat, mods] of categories.entries()) { + for (const [cat, mods] of ordered) { if (cat.toLowerCase().includes(q) || categoryLabel(cat).toLowerCase().includes(q)) { result.push([cat, mods]); } else { diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index c92a08c..be537ec 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -1396,6 +1396,7 @@ "security-tools": "Sicherheit", "fleet": "Fuhrpark", "infrastructure": "Infrastruktur", - "procurement": "Beschaffung" + "procurement": "Beschaffung", + "custom-modules": "Eigene Module" } } diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index eca1781..2787ae3 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -1396,6 +1396,7 @@ "security-tools": "Security", "fleet": "Fleet", "infrastructure": "Infrastructure", - "procurement": "Procurement" + "procurement": "Procurement", + "custom-modules": "Custom modules" } } diff --git a/apps/web/src/messages/module-categories.spec.ts b/apps/web/src/messages/module-categories.spec.ts index e1099b1..8b49f88 100644 --- a/apps/web/src/messages/module-categories.spec.ts +++ b/apps/web/src/messages/module-categories.spec.ts @@ -1,4 +1,4 @@ -import { MODULE_CATEGORIES } from '@tessera/shared'; +import { CUSTOM_MODULE_CATEGORIES } from '@tessera/shared'; import { describe, expect, it } from 'vitest'; import de from './de.json'; import en from './en.json'; @@ -9,13 +9,13 @@ import en from './en.json'; * API) braucht einen Anzeigenamen in `moduleCategories` beider Sprachen — * sonst zeigte die Seitenleiste die rohe Kennung. */ -describe('MODULE_CATEGORIES', () => { +describe('CUSTOM_MODULE_CATEGORIES (inkl. MODULE_CATEGORIES)', () => { it.each([ ['de', de], ['en', en], ] as const)('hat je Kennung einen Schluessel in moduleCategories (%s)', (_lang, messages) => { const labels = messages.moduleCategories as Record; - for (const category of MODULE_CATEGORIES) { + for (const category of CUSTOM_MODULE_CATEGORIES) { expect(labels[category], `moduleCategories.${category}`).toBeTruthy(); } }); diff --git a/docs/anleitung-anwender.md b/docs/anleitung-anwender.md index 15a2845..815992b 100644 --- a/docs/anleitung-anwender.md +++ b/docs/anleitung-anwender.md @@ -176,7 +176,7 @@ Das Modul zeigt den Zustand Ihrer Proxmox-Server auf einen Blick — für die dr - **Akzentfarbe:** Passt die Hauptfarbe der Oberfläche an Ihren Geschmack an; über „Zurücksetzen" kehren Sie zur Standardfarbe zurück. - **Passwort ändern:** Nur sichtbar und nutzbar, wenn Ihr Konto **lokal** in Tessera verwaltet wird. Wird Ihr Konto stattdessen über das Verzeichnis (LDAP/Active Directory) verwaltet, zeigt Tessera stattdessen den Hinweis „Ihr Passwort wird über das Verzeichnis (LDAP) verwaltet. Eine Änderung ist hier nicht möglich." — in diesem Fall ändern Sie Ihr Passwort über die üblichen Firmenwege (z. B. Windows-Anmeldung), nicht in Tessera. -**Allgemein > Eigene Module:** Hier nehmen Sie Webseiten, die Sie oft brauchen, als eigene Einträge in Ihre Seitenleiste auf. Diese Einträge sehen nur Sie – kein anderer Benutzer und auch kein Administrator. Mit „Eigenes Modul anlegen“ vergeben Sie einen Namen, tragen die Adresse ein (sie muss mit https:// beginnen und darf keinen Benutzernamen und kein Kennwort enthalten) und wählen die Kategorie, unter der der Eintrag in der Seitenleiste erscheint, zum Beispiel „Infrastruktur“. In der Liste ändern („Bearbeiten“) oder entfernen („Löschen“) Sie Ihre Einträge; die Seitenleiste zieht sofort nach. Ein Klick auf den Eintrag zeigt die Seite direkt in Tessera; manche Webseiten erlauben das Einbetten nicht – der Rahmen bleibt dann leer, der Knopf „In neuem Tab öffnen“ funktioniert trotzdem. Einträge, die ein Administrator für alle angelegt hat, erscheinen ebenfalls in Ihrer Seitenleiste, lassen sich hier aber nicht ändern. +**Allgemein > Eigene Module:** Hier nehmen Sie Webseiten, die Sie oft brauchen, als eigene Einträge in Ihre Seitenleiste auf. Diese Einträge sehen nur Sie – kein anderer Benutzer und auch kein Administrator. Mit „Eigenes Modul anlegen“ vergeben Sie einen Namen, tragen die Adresse ein (sie muss mit https:// beginnen und darf keinen Benutzernamen und kein Kennwort enthalten) und wählen die Kategorie, unter der der Eintrag in der Seitenleiste erscheint, zum Beispiel „Infrastruktur“ – oder „Eigene Module“, dann stehen Ihre Einträge gesammelt in einer eigenen Gruppe ganz unten in der Seitenleiste. Diese Gruppe erscheint nur, solange ein Eintrag darin liegt. In der Liste ändern („Bearbeiten“) oder entfernen („Löschen“) Sie Ihre Einträge; die Seitenleiste zieht sofort nach. Ein Klick auf den Eintrag zeigt die Seite direkt in Tessera; manche Webseiten erlauben das Einbetten nicht – der Rahmen bleibt dann leer, der Knopf „In neuem Tab öffnen“ funktioniert trotzdem. Einträge, die ein Administrator für alle angelegt hat, erscheinen ebenfalls in Ihrer Seitenleiste, lassen sich hier aber nicht ändern. **Dashboard > Widgets:** Hier finden Sie für jedes auf Ihrem Dashboard platzierte Widget die zugehörigen Einstellungen, zum Beispiel eigene Suchanbieter für die Suchleiste, beim Kalender die Monatsansicht (ein/aus), die Anzahl der angezeigten Termine (bis zu zehn, oder ausgeblendet) und den Zeitraum (7 bis 90 Tage), oder bei Notizen und Favoriten die Überschrift der Kachel. Beim Bilderrahmen verwalten Sie hier die Bilder: „Bild hochladen“ wählt eine Datei von Ihrem Rechner (PNG, JPEG, GIF oder WebP, höchstens 5 MB, bis zu 30 Bilder), „Webadresse hinzufügen“ bindet ein Bild über seine https-Adresse ein (http-Adressen werden abgewiesen). Jeder Eintrag zeigt eine Vorschau, ein Feld für die Bildunterschrift, die Pfeile „Nach oben“/„Nach unten“ für die Reihenfolge und „Bild entfernen“; ein hochgeladenes Bild wird beim Entfernen auch vom Server gelöscht. Darüber legen Sie den Bildausschnitt (ganz sichtbar oder formatfüllend), das Wechselintervall (kein Wechsel oder 5 Sekunden bis 60 Minuten) und die Reihenfolge (der Reihe nach oder zufällig) fest. Beim XFrame tragen Sie die https-Adresse der Webseite ein (http-Adressen werden abgewiesen), optional einen Titel für die Kopfleiste und wählen, ob die Seite automatisch neu geladen wird; ein dauerhafter Hinweis erinnert daran, dass manche Webseiten das Einbetten verweigern. Mit „Nur einen Ausschnitt der Seite anzeigen“ erscheint eine Vorschau der Seite, in der Sie den Rahmen verschieben und an den Ecken ziehen oder die Werte eintippen; der Ausschnitt ist eine Position auf der Seite und muss neu gesetzt werden, wenn die Seite ihren Aufbau ändert. Bei der Proxmox-Kachel legen Sie einen Titel fest und haken an, welche Server sie zeigt; ohne Haken zeigt sie alle Server. Dieselbe Auswahl erreichen Sie im Bearbeitungsmodus des Dashboards auch direkt an der Kachel über „Server auswählen“ — das ist der Weg für eine Proxmox-Kachel auf einem weiteren Reiter, denn diese Seite zeigt nur die Kacheln des ersten Reiters. diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9f17c98..29b7947 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -274,3 +274,16 @@ export const MODULE_CATEGORIES = [ ] as const; export type ModuleCategory = (typeof MODULE_CATEGORIES)[number]; + +/** + * Zusaetzliche Kategorie nur fuer eigene Module (Nutzerwunsch 29.09.2026): + * „Eigene Module“ sammelt selbst angelegte Eintraege in einer eigenen Gruppe + * der Seitenleiste. Wie jede Kategorie erscheint sie nur, wenn ein Eintrag + * darin liegt, und steht immer zuletzt. + */ +export const CUSTOM_MODULE_CATEGORY = "custom-modules" as const; + +/** Auswahl im Formular „Eigene Module“ und Pruefung in der API. */ +export const CUSTOM_MODULE_CATEGORIES = [...MODULE_CATEGORIES, CUSTOM_MODULE_CATEGORY] as const; + +export type CustomModuleCategory = (typeof CUSTOM_MODULE_CATEGORIES)[number];