style(web): eigene Module ohne Kopfzeile, "In neuem Tab oeffnen" in der App-Leiste
Name- und Hinweiszeile ueber dem Rahmen entfallen (Name steht schon in der App-Leiste, Nutzerwunsch 29.09.); der Link wandert per Portal in HEADER_ACTIONS_SLOT_ID. Ungenutzter Schluessel customModules.embedHint weg. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,10 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
|||||||
|
|
||||||
## Unveröffentlicht
|
## Unveröffentlicht
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
|
||||||
|
- Eigene Module: Die Seite füllt jetzt den ganzen Inhaltsbereich. Name und Hinweiszeile darüber sind weggefallen – der Name steht ohnehin oben in der Leiste, und „In neuem Tab öffnen“ sitzt jetzt dort rechts.
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
- Desktop-App: „Auf Version … aktualisieren“ im Menü des Tessera-Symbols scheiterte mit „Signaturprüfung fehlgeschlagen“ und öffnete stattdessen die Download-Seite, wenn der Server seit der letzten Update-Prüfung der App eine neuere Version bekommen hatte. Die App fragt jetzt beim Klick zuerst frisch nach und installiert genau die Version, die der Server in diesem Moment anbietet.
|
- Desktop-App: „Auf Version … aktualisieren“ im Menü des Tessera-Symbols scheiterte mit „Signaturprüfung fehlgeschlagen“ und öffnete stattdessen die Download-Seite, wenn der Server seit der letzten Update-Prüfung der App eine neuere Version bekommen hatte. Die App fragt jetzt beim Klick zuerst frisch nach und installiert genau die Version, die der Server in diesem Moment anbietet.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ vi.mock('next-intl', () => ({
|
|||||||
useTranslations: () => (key: string) => {
|
useTranslations: () => (key: string) => {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
openInNewTab: 'In neuem Tab öffnen',
|
openInNewTab: 'In neuem Tab öffnen',
|
||||||
embedHint: 'Manche Seiten lassen sich nicht einbetten.',
|
|
||||||
notFound: 'Dieses Modul gibt es nicht mehr.',
|
notFound: 'Dieses Modul gibt es nicht mehr.',
|
||||||
invalidUrl: 'Ungültige Adresse',
|
invalidUrl: 'Ungültige Adresse',
|
||||||
loading: 'Wird geladen',
|
loading: 'Wird geladen',
|
||||||
@@ -59,7 +58,7 @@ describe('CustomModuleView', () => {
|
|||||||
expect(mockGetCustomModule).toHaveBeenCalledWith('abc');
|
expect(mockGetCustomModule).toHaveBeenCalledWith('abc');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('zeigt Namen und immer sichtbar den Link „In neuem Tab öffnen“', async () => {
|
it('zeigt immer den Link „In neuem Tab öffnen“, aber keine Name- oder Hinweiszeile', async () => {
|
||||||
mockGetCustomModule.mockResolvedValue(mod('https://example.com/wiki'));
|
mockGetCustomModule.mockResolvedValue(mod('https://example.com/wiki'));
|
||||||
render(<CustomModuleView id="abc" />);
|
render(<CustomModuleView id="abc" />);
|
||||||
|
|
||||||
@@ -67,7 +66,21 @@ describe('CustomModuleView', () => {
|
|||||||
expect(link).toHaveAttribute('href', 'https://example.com/wiki');
|
expect(link).toHaveAttribute('href', 'https://example.com/wiki');
|
||||||
expect(link).toHaveAttribute('target', '_blank');
|
expect(link).toHaveAttribute('target', '_blank');
|
||||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||||
expect(screen.getByRole('heading', { name: 'Wiki' })).toBeInTheDocument();
|
expect(screen.queryByRole('heading')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setzt den Link in die App-Leiste, wenn deren Einhaengepunkt da ist', async () => {
|
||||||
|
const slot = document.createElement('div');
|
||||||
|
slot.id = 'header-actions-slot';
|
||||||
|
document.body.appendChild(slot);
|
||||||
|
try {
|
||||||
|
mockGetCustomModule.mockResolvedValue(mod('https://example.com/wiki'));
|
||||||
|
render(<CustomModuleView id="abc" />);
|
||||||
|
const link = await screen.findByRole('link', { name: 'In neuem Tab öffnen' });
|
||||||
|
expect(slot.contains(link)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
slot.remove();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { HEADER_ACTIONS_SLOT_ID } from '@/components/layout/header-slot';
|
||||||
import { XFRAME_SANDBOX } from '@/components/dashboard/widgets/xframe-config';
|
import { XFRAME_SANDBOX } from '@/components/dashboard/widgets/xframe-config';
|
||||||
import { type CustomModule, checkCustomModuleUrl, getCustomModule } from '@/lib/custom-modules-api';
|
import { type CustomModule, checkCustomModuleUrl, getCustomModule } from '@/lib/custom-modules-api';
|
||||||
|
|
||||||
type LoadState = { status: 'loading' } | { status: 'missing' } | { status: 'ready'; mod: CustomModule };
|
type LoadState = { status: 'loading' } | { status: 'missing' } | { status: 'ready'; mod: CustomModule };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rahmen-Ansicht eines eigenen Moduls (quick-260929-9wc, D-06): schmale
|
* Rahmen-Ansicht eines eigenen Moduls (quick-260929-9wc, D-06): der Rahmen
|
||||||
* Leiste mit Name, Hinweis und dem immer sichtbaren Knopf „In neuem Tab
|
* fuellt den ganzen Inhaltsbereich. Der Name steht ohnehin in der App-Leiste,
|
||||||
* oeffnen“, darunter der flaechenfuellende Rahmen. Sandbox-Wert und
|
* der immer sichtbare Knopf „In neuem Tab oeffnen“ sitzt dort rechts (Portal
|
||||||
|
* in HEADER_ACTIONS_SLOT_ID, wie die Dashboard-Aktionen); Name- und
|
||||||
|
* Hinweiszeile ueber dem Rahmen entfielen auf Nutzerwunsch (29.09.2026). Sandbox-Wert und
|
||||||
* Attribute wie im XFrame-Widget (`XFRAME_SANDBOX`, kein `allow`, kein
|
* Attribute wie im XFrame-Widget (`XFRAME_SANDBOX`, kein `allow`, kein
|
||||||
* Referrer). Iframe und Link entstehen NUR bei einer https-Adresse ohne
|
* Referrer). Iframe und Link entstehen NUR bei einer https-Adresse ohne
|
||||||
* Zugangsdaten — auch eine von Hand veraenderte Datenbankzeile mit
|
* Zugangsdaten — auch eine von Hand veraenderte Datenbankzeile mit
|
||||||
@@ -19,6 +23,10 @@ type LoadState = { status: 'loading' } | { status: 'missing' } | { status: 'read
|
|||||||
export function CustomModuleView({ id }: { id: string }) {
|
export function CustomModuleView({ id }: { id: string }) {
|
||||||
const t = useTranslations('customModules');
|
const t = useTranslations('customModules');
|
||||||
const [state, setState] = useState<LoadState>({ status: 'loading' });
|
const [state, setState] = useState<LoadState>({ status: 'loading' });
|
||||||
|
const [actionsSlot, setActionsSlot] = useState<HTMLElement | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setActionsSlot(document.getElementById(HEADER_ACTIONS_SLOT_ID));
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -46,24 +54,36 @@ export function CustomModuleView({ id }: { id: string }) {
|
|||||||
const { mod } = state;
|
const { mod } = state;
|
||||||
const urlOk = checkCustomModuleUrl(mod.url) === 'ok';
|
const urlOk = checkCustomModuleUrl(mod.url) === 'ok';
|
||||||
|
|
||||||
return (
|
const openLink = urlOk ? (
|
||||||
<div className="flex h-[calc(100vh-var(--header-height)-1.5rem)] min-h-[320px] flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<h1 className="truncate text-base font-semibold text-foreground">{mod.name}</h1>
|
|
||||||
{urlOk && <p className="truncate text-xs text-muted-foreground">{t('embedHint')}</p>}
|
|
||||||
</div>
|
|
||||||
{urlOk && (
|
|
||||||
<a
|
<a
|
||||||
href={mod.url}
|
href={mod.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="btn btn-secondary shrink-0"
|
className={actionsSlot ? 'btn btn-appbar' : 'btn btn-secondary self-end'}
|
||||||
>
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M15 3h6v6" />
|
||||||
|
<path d="M10 14 21 3" />
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
</svg>
|
||||||
{t('openInNewTab')}
|
{t('openInNewTab')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
) : null;
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-[calc(100vh-var(--header-height)-1.5rem)] min-h-[320px] flex-col gap-2">
|
||||||
|
{openLink && (actionsSlot ? createPortal(openLink, actionsSlot) : openLink)}
|
||||||
{urlOk ? (
|
{urlOk ? (
|
||||||
<iframe
|
<iframe
|
||||||
src={mod.url}
|
src={mod.url}
|
||||||
|
|||||||
@@ -1364,7 +1364,6 @@
|
|||||||
},
|
},
|
||||||
"customModules": {
|
"customModules": {
|
||||||
"openInNewTab": "In neuem Tab öffnen",
|
"openInNewTab": "In neuem Tab öffnen",
|
||||||
"embedHint": "Manche Seiten lassen sich nicht einbetten. Öffnen Sie die Seite dann in einem neuen Tab.",
|
|
||||||
"notFound": "Dieses Modul gibt es nicht mehr.",
|
"notFound": "Dieses Modul gibt es nicht mehr.",
|
||||||
"invalidUrl": "Die Adresse dieses Moduls ist keine gültige https-Adresse und wird deshalb nicht angezeigt.",
|
"invalidUrl": "Die Adresse dieses Moduls ist keine gültige https-Adresse und wird deshalb nicht angezeigt.",
|
||||||
"loading": "Wird geladen …",
|
"loading": "Wird geladen …",
|
||||||
|
|||||||
@@ -1364,7 +1364,6 @@
|
|||||||
},
|
},
|
||||||
"customModules": {
|
"customModules": {
|
||||||
"openInNewTab": "Open in new tab",
|
"openInNewTab": "Open in new tab",
|
||||||
"embedHint": "Some pages cannot be embedded. If this one stays blank, open it in a new tab.",
|
|
||||||
"notFound": "This module no longer exists.",
|
"notFound": "This module no longer exists.",
|
||||||
"invalidUrl": "This module's address is not a valid https address and is therefore not shown.",
|
"invalidUrl": "This module's address is not a valid https address and is therefore not shown.",
|
||||||
"loading": "Loading …",
|
"loading": "Loading …",
|
||||||
|
|||||||
Reference in New Issue
Block a user