feat(quick-261008-who): Vorladen schalten, Grenze acht mit gepinnten Modulen
- Offenhalten-Grenze fuenf auf acht, gepinnte Module nie per LRU verworfen - Schalter in der App-Leiste der Modulansicht, Karte in Einstellungen -> Eigene Module - Texte de/en Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
'use client';
|
||||
|
||||
import { CUSTOM_MODULE_PRELOAD_MAX } from '@tessera/shared';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { SettingsSection } from '@/components/control-center/settings-section';
|
||||
import {
|
||||
checkCustomModuleUrl,
|
||||
type CustomModule,
|
||||
CustomModuleRequestError,
|
||||
listCustomModules,
|
||||
setCustomModulePreload,
|
||||
} from '@/lib/custom-modules-api';
|
||||
import { useCustomModuleCacheStore } from '@/lib/stores/custom-module-cache-store';
|
||||
import { useMarketplaceStore } from '@/lib/stores/marketplace-store';
|
||||
|
||||
/**
|
||||
* Karte „Beim Start vorladen“ in Einstellungen → Eigene Module
|
||||
* (quick-261008-who). Zeigt ALLE fuer den Benutzer sichtbaren Module
|
||||
* (gemeinsame und persoenliche) mit je einem Kontrollkaestchen und dem Zaehler
|
||||
* „x von 8“ — die Verwaltungsliste darueber zeigt nur persoenliche Eintraege,
|
||||
* beim Erreichen der Grenze muss man aber sehen, welche Module schon vorgeladen
|
||||
* werden. Die Wahl liegt serverseitig pro Benutzer (PUT .../preload); bei acht
|
||||
* gewaehlten sind die uebrigen Kaestchen gesperrt. Zieht nach Anlegen, Aendern
|
||||
* oder Loeschen (`sidebarRefreshKey`) neu.
|
||||
*/
|
||||
export function CustomModulePreloadSettings() {
|
||||
const t = useTranslations('customModules.preload');
|
||||
const tCommon = useTranslations('common');
|
||||
const refreshKey = useMarketplaceStore((s) => s.sidebarRefreshKey);
|
||||
const [modules, setModules] = useState<CustomModule[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<{ text: string; error: boolean } | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setModules(await listCustomModules());
|
||||
setLoadFailed(false);
|
||||
} catch {
|
||||
setLoadFailed(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: refreshKey ist der Ausloeser fuer das Neuladen.
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load, refreshKey]);
|
||||
|
||||
const count = modules.filter((m) => m.preload === true).length;
|
||||
const full = count >= CUSTOM_MODULE_PRELOAD_MAX;
|
||||
|
||||
const toggle = async (mod: CustomModule) => {
|
||||
const next = mod.preload !== true;
|
||||
setNotice(null);
|
||||
setBusyId(mod.id);
|
||||
try {
|
||||
await setCustomModulePreload(mod.id, next);
|
||||
setModules((all) => all.map((m) => (m.id === mod.id ? { ...m, preload: next } : m)));
|
||||
const store = useCustomModuleCacheStore.getState();
|
||||
if (!next) store.setPinned(mod.id, false);
|
||||
else if (checkCustomModuleUrl(mod.url) === 'ok') {
|
||||
store.preload({ id: mod.id, name: mod.name, url: mod.url });
|
||||
}
|
||||
} catch (err) {
|
||||
const limit = err instanceof CustomModuleRequestError && err.status === 409;
|
||||
setNotice({
|
||||
text: limit ? t('limitReached', { max: CUSTOM_MODULE_PRELOAD_MAX }) : t('saveError'),
|
||||
error: !limit,
|
||||
});
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('settingsTitle')}
|
||||
description={t('settingsDescription', { max: CUSTOM_MODULE_PRELOAD_MAX })}
|
||||
>
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground">{tCommon('loading')}</p>
|
||||
) : loadFailed ? (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{t('loadError')}
|
||||
</p>
|
||||
) : modules.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">{t('empty')}</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted-foreground" data-testid="preload-count">
|
||||
{t('count', { count, max: CUSTOM_MODULE_PRELOAD_MAX })}
|
||||
</p>
|
||||
{full && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('limitReached', { max: CUSTOM_MODULE_PRELOAD_MAX })}
|
||||
</p>
|
||||
)}
|
||||
<ul className="flex flex-col divide-y divide-border">
|
||||
{modules.map((mod) => {
|
||||
const checked = mod.preload === true;
|
||||
const inputId = `preload-${mod.id}`;
|
||||
return (
|
||||
<li key={mod.id} className="flex items-center gap-3 py-2">
|
||||
<input
|
||||
id={inputId}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={busyId !== null || (full && !checked)}
|
||||
onChange={() => toggle(mod)}
|
||||
/>
|
||||
<label htmlFor={inputId} className="min-w-0 flex-1 truncate text-sm">
|
||||
{mod.name}
|
||||
</label>
|
||||
<span className="shrink-0 text-xs text-muted-foreground">
|
||||
{mod.personal ? t('personal') : t('shared')}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{notice && (
|
||||
<p
|
||||
role="status"
|
||||
className={`text-sm ${notice.error ? 'text-destructive' : 'text-muted-foreground'}`}
|
||||
>
|
||||
{notice.text}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user