feat(settings): complete privacy usage and data recovery presentation
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Alert, Box, Button, FormControlLabel, Skeleton, Switch, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
import { api } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useToast } from "../toast";
|
||||
import { checkSessionCookie, getSessionContext, isSessionInvalidated } from "../sessionBoundary";
|
||||
|
||||
type AiSettings = {
|
||||
enabled: boolean;
|
||||
@@ -17,19 +18,47 @@ export default function AiPrivacySettingsCard() {
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const [settings, setSettings] = useState<AiSettings | null>(null);
|
||||
const [savedSettings, setSavedSettings] = useState<AiSettings | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [readAttempt, setReadAttempt] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const session = useRef({ mounted: true, generation: 0, saving: false });
|
||||
const currentTranslation = useRef(t);
|
||||
const body = useRef<HTMLDivElement>(null);
|
||||
const retryFocus = useRef<HTMLElement | null>(null);
|
||||
const dirty = Boolean(settings && savedSettings && (settings.enabled !== savedSettings.enabled || settings.externalProcessingAllowed !== savedSettings.externalProcessingAllowed));
|
||||
useEffect(() => { currentTranslation.current = t; }, [t]);
|
||||
useEffect(() => {
|
||||
if (!loading && settings && retryFocus.current) {
|
||||
if (document.activeElement === document.body || document.activeElement === retryFocus.current) body.current?.focus();
|
||||
retryFocus.current = null;
|
||||
}
|
||||
}, [loading, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const owner = session.current;
|
||||
owner.mounted = true;
|
||||
const generation = ++owner.generation;
|
||||
const context = getSessionContext();
|
||||
const active = () => owner.mounted && owner.generation === generation && !isSessionInvalidated() && getSessionContext() === context;
|
||||
api.get<AiSettings>("/ai/settings")
|
||||
.then((response) => { if (active) setSettings(response.data); })
|
||||
.catch(() => { if (active) setFailed(true); });
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
.then((response) => { if (active()) { setSettings(response.data); setSavedSettings(response.data); } })
|
||||
.catch(() => { if (active()) setFailed(true); })
|
||||
.finally(() => { if (active()) setLoading(false); });
|
||||
return () => { owner.mounted = false; owner.generation += 1; };
|
||||
}, [readAttempt]);
|
||||
|
||||
const save = async () => {
|
||||
if (!settings) return;
|
||||
checkSessionCookie();
|
||||
const owner = session.current;
|
||||
if (!settings || !owner.mounted || owner.saving || isSessionInvalidated()) return;
|
||||
const submitted = settings;
|
||||
const context = getSessionContext();
|
||||
const generation = owner.generation;
|
||||
const active = () => owner.mounted && owner.generation === generation && !isSessionInvalidated() && getSessionContext() === context;
|
||||
owner.saving = true;
|
||||
if (body.current?.contains(document.activeElement)) body.current.focus();
|
||||
setSaving(true);
|
||||
setFailed(false);
|
||||
try {
|
||||
@@ -37,19 +66,21 @@ export default function AiPrivacySettingsCard() {
|
||||
enabled: settings.enabled,
|
||||
externalProcessingAllowed: settings.externalProcessingAllowed,
|
||||
});
|
||||
setSettings(response.data);
|
||||
toast(t("settingsAiSaved"), "success");
|
||||
if (!active()) return;
|
||||
setSavedSettings(response.data);
|
||||
setSettings(current => current === submitted || !current ? response.data : { ...response.data, enabled: current.enabled, externalProcessingAllowed: current.externalProcessingAllowed });
|
||||
toast(currentTranslation.current("settingsAiSaved"), "success");
|
||||
} catch {
|
||||
setFailed(true);
|
||||
if (active()) setFailed(true);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
if (active()) { owner.saving = false; setSaving(false); }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSection surface title={t("settingsAiPrivacyTitle")} description={t("settingsAiPrivacyBody")}>
|
||||
{failed ? <Alert severity="error" sx={{ mb: 1.5 }}>{t("settingsAiPrivacyUnavailable")}</Alert> : null}
|
||||
{!settings ? <Skeleton variant="rounded" height={96} /> : <Box sx={{ display: "grid", gap: 1 }}>
|
||||
{failed ? <Alert severity="error" sx={{ mb: 1.5 }} action={!settings ? <Button color="inherit" disabled={loading} onClick={() => { retryFocus.current = document.activeElement as HTMLElement; setFailed(false); setLoading(true); setReadAttempt(value => value + 1); }}>{t("retry")}</Button> : undefined}>{t(settings ? "settingsAiSaveFailed" : "settingsAiPrivacyUnavailable")}</Alert> : null}
|
||||
{!settings ? loading ? <Box role="status" aria-label={t("loading")}><Skeleton variant="rounded" height={96} /></Box> : null : <Box ref={body} role="group" aria-label={t("settingsAiPrivacyTitle")} aria-busy={saving} tabIndex={-1} sx={{ display: "grid", gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={settings.enabled} onChange={(event) => setSettings({ ...settings, enabled: event.target.checked })} />}
|
||||
label={t("settingsAiEnabled")}
|
||||
@@ -67,7 +98,10 @@ export default function AiPrivacySettingsCard() {
|
||||
? t("settingsAiExternalAvailable")
|
||||
: t("settingsAiExternalLocalOnly")}
|
||||
</Typography>
|
||||
<Box><Button variant="contained" disabled={saving} onClick={() => void save()}>{t("settingsAiSave")}</Button></Box>
|
||||
{dirty ? <Typography role="status" variant="body2" color="text.secondary">{t("assetsUnsavedChanges")}</Typography> : null}
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}><Button variant="contained" disabled={saving} onClick={() => void save()}>{t(saving ? "saving" : "settingsAiSave")}</Button>
|
||||
{dirty ? <Button disabled={saving} onClick={() => { body.current?.focus(); setSettings(savedSettings); setFailed(false); }}>{t("assetsDiscardChanges")}</Button> : null}
|
||||
</Box>
|
||||
</Box>}
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user