fix(settings): show including state on library index include action

Mirror the exclude UX: flushSync before bootstrap, block repeat clicks,
show "Including…" on the row, and roll back exclusion if bind fails.
This commit is contained in:
Maxim Isaev
2026-05-22 14:16:19 +03:00
parent 60aad12cd4
commit 376edbfe18
11 changed files with 56 additions and 15 deletions
+42 -15
View File
@@ -60,6 +60,7 @@ export default function LibraryIndexSection() {
const [progressByServer, setProgressByServer] = useState<Record<string, string | null>>({}); const [progressByServer, setProgressByServer] = useState<Record<string, string | null>>({});
const [busyServerId, setBusyServerId] = useState<string | null>(null); const [busyServerId, setBusyServerId] = useState<string | null>(null);
const [excludingServerId, setExcludingServerId] = useState<string | null>(null); const [excludingServerId, setExcludingServerId] = useState<string | null>(null);
const [includingServerId, setIncludingServerId] = useState<string | null>(null);
const [bootstrapping, setBootstrapping] = useState(false); const [bootstrapping, setBootstrapping] = useState(false);
const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null); const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -133,6 +134,7 @@ export default function LibraryIndexSection() {
setProgressByServer({}); setProgressByServer({});
setBusyServerId(null); setBusyServerId(null);
setExcludingServerId(null); setExcludingServerId(null);
setIncludingServerId(null);
return; return;
} }
void runBootstrap(); void runBootstrap();
@@ -251,22 +253,38 @@ export default function LibraryIndexSection() {
}; };
const handleIncludeServer = async (serverId: string) => { const handleIncludeServer = async (serverId: string) => {
setServerSyncExcluded(serverId, false); if (includingServerId || excludingServerId) return;
const srv = servers.find(s => s.id === serverId); const srv = servers.find(s => s.id === serverId);
if (srv) { if (!srv) return;
setBootstrapping(true); flushSync(() => {
try { setIncludingServerId(serverId);
const result = await bootstrapIndexedServer(srv); setServerSyncExcluded(serverId, false);
applyConnectionResults({ [serverId]: result }); });
await refreshAllStatuses(); try {
} finally { const result = await bootstrapIndexedServer(srv);
setBootstrapping(false); applyConnectionResults({ [serverId]: result });
if (result === 'error') {
setServerSyncExcluded(serverId, true);
showToast(t('settings.libraryIndexBindError', { error: t('settings.libraryIndexStatusError') }), 5000, 'error');
return;
} }
try {
const fresh = await libraryGetStatus(serverId);
syncPhaseRef.current[serverId] = fresh.syncPhase;
setStatusByServer(prev => ({ ...prev, [serverId]: fresh }));
} catch {
/* status poll is best-effort */
}
} catch (e) {
setServerSyncExcluded(serverId, true);
showToast(t('settings.libraryIndexBindError', { error: String(e) }), 5000, 'error');
} finally {
setIncludingServerId(null);
} }
}; };
const handleExcludeServer = async (serverId: string) => { const handleExcludeServer = async (serverId: string) => {
if (excludingServerId) return; if (excludingServerId || includingServerId) return;
flushSync(() => setExcludingServerId(serverId)); flushSync(() => setExcludingServerId(serverId));
try { try {
const syncing = const syncing =
@@ -316,7 +334,8 @@ export default function LibraryIndexSection() {
} }
}; };
const globalBusy = bootstrapping || busyServerId != null || excludingServerId != null; const globalBusy =
bootstrapping || busyServerId != null || excludingServerId != null || includingServerId != null;
return ( return (
<SettingsSubSection <SettingsSubSection
@@ -344,7 +363,7 @@ export default function LibraryIndexSection() {
<input <input
type="checkbox" type="checkbox"
checked={masterEnabled} checked={masterEnabled}
disabled={servers.length === 0 || bootstrapping} disabled={servers.length === 0 || bootstrapping || includingServerId != null || excludingServerId != null}
onChange={e => void handleMasterToggle(e.target.checked)} onChange={e => void handleMasterToggle(e.target.checked)}
/> />
<span className="toggle-track" /> <span className="toggle-track" />
@@ -373,9 +392,12 @@ export default function LibraryIndexSection() {
connection={connectionByServer[srv.id] ?? 'unknown'} connection={connectionByServer[srv.id] ?? 'unknown'}
progressLabel={progressByServer[srv.id] ?? null} progressLabel={progressByServer[srv.id] ?? null}
busy={busyServerId === srv.id} busy={busyServerId === srv.id}
including={includingServerId === srv.id}
excluding={excludingServerId === srv.id} excluding={excludingServerId === srv.id}
actionsDisabled={ actionsDisabled={
(globalBusy && busyServerId !== srv.id) || excludingServerId != null (globalBusy && busyServerId !== srv.id)
|| excludingServerId != null
|| includingServerId != null
} }
onFullSync={() => void runServerAction(srv.id, 'full')} onFullSync={() => void runServerAction(srv.id, 'full')}
onDeltaSync={() => void runServerAction(srv.id, 'delta')} onDeltaSync={() => void runServerAction(srv.id, 'delta')}
@@ -403,10 +425,15 @@ export default function LibraryIndexSection() {
type="button" type="button"
className="btn btn-surface" className="btn btn-surface"
style={{ fontSize: 12, padding: '4px 10px' }} style={{ fontSize: 12, padding: '4px 10px' }}
disabled={bootstrapping || excludingServerId != null} disabled={
includingServerId != null || excludingServerId != null
}
aria-busy={includingServerId === srv.id}
onClick={() => void handleIncludeServer(srv.id)} onClick={() => void handleIncludeServer(srv.id)}
> >
{t('settings.libraryIndexIncludeServer')} {includingServerId === srv.id
? t('settings.libraryIndexIncludingServer')
: t('settings.libraryIndexIncludeServer')}
</button> </button>
</div> </div>
))} ))}
@@ -18,6 +18,7 @@ interface LibraryIndexServerRowProps {
connection: LibraryServerConnection; connection: LibraryServerConnection;
progressLabel: string | null; progressLabel: string | null;
busy: boolean; busy: boolean;
including: boolean;
excluding: boolean; excluding: boolean;
actionsDisabled: boolean; actionsDisabled: boolean;
onFullSync: () => void; onFullSync: () => void;
@@ -34,6 +35,7 @@ export default function LibraryIndexServerRow({
connection, connection,
progressLabel, progressLabel,
busy, busy,
including,
excluding, excluding,
actionsDisabled, actionsDisabled,
onFullSync, onFullSync,
@@ -93,6 +95,9 @@ export default function LibraryIndexServerRow({
{busy && ( {busy && (
<span style={{ fontSize: 11, color: 'var(--accent)' }}>{t('settings.libraryIndexServerSyncing')}</span> <span style={{ fontSize: 11, color: 'var(--accent)' }}>{t('settings.libraryIndexServerSyncing')}</span>
)} )}
{including && !busy && (
<span style={{ fontSize: 11, color: 'var(--accent)' }}>{t('settings.libraryIndexIncludingServer')}</span>
)}
</div> </div>
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 4, lineHeight: 1.45 }}> <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 4, lineHeight: 1.45 }}>
{phaseLabel} {phaseLabel}
+1
View File
@@ -264,6 +264,7 @@ export const settings = {
libraryIndexExcludingServer: 'Wird ausgeschlossen…', libraryIndexExcludingServer: 'Wird ausgeschlossen…',
libraryIndexExcludedTitle: 'Von Synchronisation ausgeschlossen', libraryIndexExcludedTitle: 'Von Synchronisation ausgeschlossen',
libraryIndexIncludeServer: 'Wieder einschließen', libraryIndexIncludeServer: 'Wieder einschließen',
libraryIndexIncludingServer: 'Wird eingeschlossen…',
libraryIndexStatus: 'Status', libraryIndexStatus: 'Status',
libraryIndexStatusIdle: 'Bereit', libraryIndexStatusIdle: 'Bereit',
libraryIndexStatusProbing: 'Server wird geprüft…', libraryIndexStatusProbing: 'Server wird geprüft…',
+1
View File
@@ -267,6 +267,7 @@ export const settings = {
libraryIndexExcludingServer: 'Excluding…', libraryIndexExcludingServer: 'Excluding…',
libraryIndexExcludedTitle: 'Excluded from sync', libraryIndexExcludedTitle: 'Excluded from sync',
libraryIndexIncludeServer: 'Include again', libraryIndexIncludeServer: 'Include again',
libraryIndexIncludingServer: 'Including…',
libraryIndexStatus: 'Status', libraryIndexStatus: 'Status',
libraryIndexStatusIdle: 'Idle', libraryIndexStatusIdle: 'Idle',
libraryIndexStatusProbing: 'Checking server…', libraryIndexStatusProbing: 'Checking server…',
+1
View File
@@ -262,6 +262,7 @@ export const settings = {
libraryIndexExcludingServer: 'Excluyendo…', libraryIndexExcludingServer: 'Excluyendo…',
libraryIndexExcludedTitle: 'Excluidos de la sincronización', libraryIndexExcludedTitle: 'Excluidos de la sincronización',
libraryIndexIncludeServer: 'Incluir de nuevo', libraryIndexIncludeServer: 'Incluir de nuevo',
libraryIndexIncludingServer: 'Incluyendo…',
libraryIndexStatus: 'Estado', libraryIndexStatus: 'Estado',
libraryIndexStatusIdle: 'Inactivo', libraryIndexStatusIdle: 'Inactivo',
libraryIndexStatusProbing: 'Comprobando servidor…', libraryIndexStatusProbing: 'Comprobando servidor…',
+1
View File
@@ -260,6 +260,7 @@ export const settings = {
libraryIndexExcludingServer: 'Exclusion…', libraryIndexExcludingServer: 'Exclusion…',
libraryIndexExcludedTitle: 'Exclus de la synchronisation', libraryIndexExcludedTitle: 'Exclus de la synchronisation',
libraryIndexIncludeServer: 'Réinclure', libraryIndexIncludeServer: 'Réinclure',
libraryIndexIncludingServer: 'Réinclusion…',
libraryIndexStatus: 'État', libraryIndexStatus: 'État',
libraryIndexStatusIdle: 'Inactif', libraryIndexStatusIdle: 'Inactif',
libraryIndexStatusProbing: 'Vérification du serveur…', libraryIndexStatusProbing: 'Vérification du serveur…',
+1
View File
@@ -259,6 +259,7 @@ export const settings = {
libraryIndexExcludingServer: 'Ekskluderer…', libraryIndexExcludingServer: 'Ekskluderer…',
libraryIndexExcludedTitle: 'Ekskludert fra synkronisering', libraryIndexExcludedTitle: 'Ekskludert fra synkronisering',
libraryIndexIncludeServer: 'Inkluder igjen', libraryIndexIncludeServer: 'Inkluder igjen',
libraryIndexIncludingServer: 'Inkluderer…',
libraryIndexStatus: 'Status', libraryIndexStatus: 'Status',
libraryIndexStatusIdle: 'Inaktiv', libraryIndexStatusIdle: 'Inaktiv',
libraryIndexStatusProbing: 'Sjekker server…', libraryIndexStatusProbing: 'Sjekker server…',
+1
View File
@@ -260,6 +260,7 @@ export const settings = {
libraryIndexExcludingServer: 'Uitsluiten…', libraryIndexExcludingServer: 'Uitsluiten…',
libraryIndexExcludedTitle: 'Uitgesloten van synchronisatie', libraryIndexExcludedTitle: 'Uitgesloten van synchronisatie',
libraryIndexIncludeServer: 'Weer opnemen', libraryIndexIncludeServer: 'Weer opnemen',
libraryIndexIncludingServer: 'Opnemen…',
libraryIndexStatus: 'Status', libraryIndexStatus: 'Status',
libraryIndexStatusIdle: 'Inactief', libraryIndexStatusIdle: 'Inactief',
libraryIndexStatusProbing: 'Server controleren…', libraryIndexStatusProbing: 'Server controleren…',
+1
View File
@@ -266,6 +266,7 @@ export const settings = {
libraryIndexExcludingServer: 'Se exclude…', libraryIndexExcludingServer: 'Se exclude…',
libraryIndexExcludedTitle: 'Excluse din sincronizare', libraryIndexExcludedTitle: 'Excluse din sincronizare',
libraryIndexIncludeServer: 'Include din nou', libraryIndexIncludeServer: 'Include din nou',
libraryIndexIncludingServer: 'Se include…',
libraryIndexStatus: 'Stare', libraryIndexStatus: 'Stare',
libraryIndexStatusIdle: 'Inactiv', libraryIndexStatusIdle: 'Inactiv',
libraryIndexStatusProbing: 'Se verifică serverul…', libraryIndexStatusProbing: 'Se verifică serverul…',
+1
View File
@@ -272,6 +272,7 @@ export const settings = {
libraryIndexExcludingServer: 'Отключается…', libraryIndexExcludingServer: 'Отключается…',
libraryIndexExcludedTitle: 'Исключены из синхронизации', libraryIndexExcludedTitle: 'Исключены из синхронизации',
libraryIndexIncludeServer: 'Включить снова', libraryIndexIncludeServer: 'Включить снова',
libraryIndexIncludingServer: 'Подключается…',
libraryIndexStatus: 'Статус', libraryIndexStatus: 'Статус',
libraryIndexStatusIdle: 'Ожидание', libraryIndexStatusIdle: 'Ожидание',
libraryIndexStatusProbing: 'Проверка сервера…', libraryIndexStatusProbing: 'Проверка сервера…',
+1
View File
@@ -259,6 +259,7 @@ export const settings = {
libraryIndexExcludingServer: '正在排除…', libraryIndexExcludingServer: '正在排除…',
libraryIndexExcludedTitle: '已排除同步', libraryIndexExcludedTitle: '已排除同步',
libraryIndexIncludeServer: '重新纳入', libraryIndexIncludeServer: '重新纳入',
libraryIndexIncludingServer: '正在纳入…',
libraryIndexStatus: '状态', libraryIndexStatus: '状态',
libraryIndexStatusIdle: '空闲', libraryIndexStatusIdle: '空闲',
libraryIndexStatusProbing: '正在检查服务器…', libraryIndexStatusProbing: '正在检查服务器…',