feat: task trash, triage trash and retention settings for every area; admin can forbid keeping things for good
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a927a94a95
commit
5a9c5bac37
36 files changed
+1356
-175
No files matched your search
@@ -19,8 +19,9 @@ import {
|
||||
Copy,
|
||||
Archive,
|
||||
Mic,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
import { AuthStatus, NotificationEvents, NotificationSettings, PushDevice, User, UserTheme } from '../../lib/types';
|
||||
import { AuthStatus, NotificationEvents, NotificationSettings, PushDevice, Retention, User, UserTheme } from '../../lib/types';
|
||||
import { ApiKeysPanel } from './ApiKeysPanel';
|
||||
import { ServerBackupsCard } from './ServerBackupsCard';
|
||||
import { saveBlob } from '../../lib/download';
|
||||
@@ -68,7 +69,25 @@ interface UserSettingsViewProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'voice' | 'webhooks' | 'data';
|
||||
// Kontoeinstellungen → Aufbewahrung: the areas and the delays offered (storage.RetentionChoices)
|
||||
const RETENTION_AREAS: { key: keyof Retention; title: string; text: string; verb: string }[] = [
|
||||
{ key: 'doneTasks', title: 'Erledigte Aufgaben', text: 'Wandern danach in den Papierkorb der Aufgaben.', verb: 'in den Papierkorb nach' },
|
||||
{ key: 'taskTrash', title: 'Papierkorb der Aufgaben', text: 'Gelöschte und geleerte Aufgaben.', verb: 'endgültig löschen nach' },
|
||||
{ key: 'triageTrash', title: 'Papierkorb der Triage', text: 'Beim Sortieren verworfene Gedanken.', verb: 'endgültig löschen nach' },
|
||||
{ key: 'noteTrash', title: 'Papierkorb der Notizen', text: 'Gelöschte Notizen im Workspace.', verb: 'endgültig löschen nach' },
|
||||
{ key: 'listDone', title: 'Abgehakte Listeneinträge', text: 'In Einkaufs- und Checklisten.', verb: 'löschen nach' },
|
||||
];
|
||||
const RETENTION_OPTIONS = [
|
||||
{ days: 7, label: '7 Tagen' },
|
||||
{ days: 14, label: '14 Tagen' },
|
||||
{ days: 30, label: '30 Tagen' },
|
||||
{ days: 90, label: '90 Tagen' },
|
||||
{ days: 365, label: 'einem Jahr' },
|
||||
{ days: 730, label: 'zwei Jahren' },
|
||||
{ days: 0, label: 'nie' },
|
||||
];
|
||||
|
||||
type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'voice' | 'retention' | 'webhooks' | 'data';
|
||||
|
||||
export function buildNotificationSettings(user: User): NotificationSettings {
|
||||
const hasLegacy = Array.isArray(user.alarmChannels);
|
||||
@@ -140,6 +159,22 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
useEffect(() => {
|
||||
void loadVoiceConfig().then(() => setVoiceChoice(voiceChoiceAvailable()));
|
||||
}, []);
|
||||
// Retention: one delay per area (storage/retention.go); "nie" only when the server allows it
|
||||
const [retentionSaving, setRetentionSaving] = useState(false);
|
||||
const [retentionError, setRetentionError] = useState<string | null>(null);
|
||||
const handleChangeRetention = async (key: keyof Retention, days: number) => {
|
||||
if (!currentUser.retention) return;
|
||||
setRetentionSaving(true);
|
||||
setRetentionError(null);
|
||||
try {
|
||||
onUserUpdated(await api.updateSettings({ retention: { ...currentUser.retention, [key]: days } }));
|
||||
} catch (err: any) {
|
||||
setRetentionError(err?.message || 'Speichern fehlgeschlagen');
|
||||
} finally {
|
||||
setRetentionSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeVoiceInput = async (next: '' | 'browser') => {
|
||||
setVoiceSaving(true);
|
||||
try {
|
||||
@@ -675,6 +710,21 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setSection('retention')}
|
||||
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
|
||||
section === 'retention'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-panel-hover'
|
||||
}`}
|
||||
>
|
||||
<Clock className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-semibold">Aufbewahrung</p>
|
||||
<p className="text-[10px] text-faint">Erledigtes & Papierkörbe aufräumen</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setSection('webhooks')}
|
||||
className={`w-full px-3.5 py-3 text-left text-xs font-medium flex items-center gap-2.5 transition-colors ${
|
||||
@@ -1964,6 +2014,55 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* SECTION: AUFBEWAHRUNG */}
|
||||
{/* ================================================================= */}
|
||||
{section === 'retention' && (
|
||||
<div className="space-y-5">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-main">Aufbewahrung</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Wie lange Erledigtes und Gelöschtes bleibt, gezählt ab dem Abhaken bzw. Löschen. In geteilten Aufgabenlisten,
|
||||
Listen und Notizen gilt die Einstellung der Person, der sie gehören.
|
||||
</p>
|
||||
</div>
|
||||
{retentionError && (
|
||||
<div className="alert-err flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
|
||||
<span>{retentionError}</span>
|
||||
</div>
|
||||
)}
|
||||
{currentUser.retention && (
|
||||
<div className="divide-y divide-border">
|
||||
{RETENTION_AREAS.map((area) => (
|
||||
<div key={area.key} className="py-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-main">{area.title}</p>
|
||||
<p className="text-muted">{area.text}</p>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className="text-muted">{area.verb}</span>
|
||||
<select
|
||||
value={currentUser.retention![area.key]}
|
||||
disabled={retentionSaving}
|
||||
onChange={(e) => handleChangeRetention(area.key, Number(e.target.value))}
|
||||
className="h-11 sm:h-8 px-2 input-field text-base sm:text-xs bg-panel"
|
||||
aria-label={`${area.title}: ${area.verb}`}
|
||||
>
|
||||
{RETENTION_OPTIONS.filter((o) => o.days > 0 || currentUser.retentionNeverAllowed !== false).map((o) => (
|
||||
<option key={o.days} value={o.days}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* SECTION 5: EXPORT & IMPORT */}
|
||||
{/* ================================================================= */}
|
||||
|
||||
Reference in new issue
Block a user