fix(review): resolve deep-review findings across quickfix batch #2
- Button: `loading` always disables (|| not ??) — stops a double-submit when a call-site passes an explicit boolean `disabled` (VideoEditor). - playlists: track un-ingestable-but-live YouTube ids in the sync baseline so a linked playlist containing a private/deleted video converges instead of re-classifying it as a pending pull forever; re-attempt ingestion each sync so a restored (private->public) video heals, and prune ids that leave YouTube. - SettingsPanel: download-auth section renders an honest error/loading state (StateMessage + retry) instead of a bare empty section on a failed credentials fetch; HU+EN loadError string. - tests: reorder-order, partial-failure baseline hold, un-ingestable converge/heal/prune, Button loading-disables, download-auth error/retry.
This commit is contained in:
@@ -14,6 +14,7 @@ import { inputCls, Section, SettingRow, Switch } from "./ui/form";
|
||||
import { Button } from "./ui/Button";
|
||||
import { type SaveState } from "./ui/DraftSaveBar";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import { StateMessage } from "./QueryState";
|
||||
import { useWizard } from "./WizardProvider";
|
||||
import { usePrefs } from "./PrefsProvider";
|
||||
import Tabs from "./Tabs";
|
||||
@@ -633,91 +634,107 @@ export function DownloadAuth() {
|
||||
return (
|
||||
<Section title={t("settings.downloadAuth.title")}>
|
||||
<p className="text-xs text-muted leading-relaxed mb-2">{t("settings.downloadAuth.intro")}</p>
|
||||
<div className="divide-y divide-border">
|
||||
{hosts.map((h) => {
|
||||
const when = h.updated_at ? new Date(h.updated_at).toLocaleDateString(i18n.language) : "";
|
||||
const open = openHost === h.host_key;
|
||||
return (
|
||||
<div key={h.host_key} className="py-2" data-testid={`cookie-host-${h.host_key}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium">{h.label}</div>
|
||||
<p className="text-xs text-muted mt-0.5" data-testid={`cookie-status-${h.host_key}`}>
|
||||
{h.configured
|
||||
? t("settings.downloadAuth.configured", { when })
|
||||
: t("settings.downloadAuth.notSet")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{h.configured && (
|
||||
<Button
|
||||
variant="danger-outline"
|
||||
size="sm"
|
||||
onClick={() => onRemove(h.host_key)}
|
||||
disabled={remove.isPending}
|
||||
{/* The host list is server-provided (always non-empty on success), so an empty `hosts` means
|
||||
the fetch failed or is still loading — render an honest state, never a bare empty section
|
||||
that reads as "nothing to configure here". */}
|
||||
{q.isError && hosts.length === 0 ? (
|
||||
<StateMessage tone="error" onRetry={() => q.refetch()}>
|
||||
{t("settings.downloadAuth.loadError")}
|
||||
</StateMessage>
|
||||
) : q.isLoading ? (
|
||||
<StateMessage>{t("common.loading")}</StateMessage>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{hosts.map((h) => {
|
||||
const when = h.updated_at
|
||||
? new Date(h.updated_at).toLocaleDateString(i18n.language)
|
||||
: "";
|
||||
const open = openHost === h.host_key;
|
||||
return (
|
||||
<div key={h.host_key} className="py-2" data-testid={`cookie-host-${h.host_key}`}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium">{h.label}</div>
|
||||
<p
|
||||
className="text-xs text-muted mt-0.5"
|
||||
data-testid={`cookie-status-${h.host_key}`}
|
||||
>
|
||||
{t("settings.downloadAuth.remove")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setOpenHost(open ? null : h.host_key);
|
||||
setDraft("");
|
||||
}}
|
||||
>
|
||||
{h.configured
|
||||
? t("settings.downloadAuth.update")
|
||||
: t("settings.downloadAuth.set")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{/* Write-only: the field is ALWAYS empty on open — the stored blob is never
|
||||
fetched or prefilled. */}
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
rows={5}
|
||||
placeholder={t("settings.downloadAuth.placeholder")}
|
||||
className={`${inputCls} font-mono text-xs resize-y`}
|
||||
data-testid={`cookie-input-${h.host_key}`}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="text-2xs text-muted leading-relaxed">
|
||||
{t("settings.downloadAuth.help")}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
{h.configured
|
||||
? t("settings.downloadAuth.configured", { when })
|
||||
: t("settings.downloadAuth.notSet")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{h.configured && (
|
||||
<Button
|
||||
variant="danger-outline"
|
||||
size="sm"
|
||||
onClick={() => onRemove(h.host_key)}
|
||||
disabled={remove.isPending}
|
||||
>
|
||||
{t("settings.downloadAuth.remove")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={!draft.trim() || save.isPending}
|
||||
onClick={() => save.mutate({ host: h.host_key, cookies: draft })}
|
||||
>
|
||||
{save.isPending
|
||||
? t("settings.downloadAuth.saving")
|
||||
: t("settings.downloadAuth.save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setOpenHost(null);
|
||||
setOpenHost(open ? null : h.host_key);
|
||||
setDraft("");
|
||||
}}
|
||||
>
|
||||
{t("settings.downloadAuth.cancel")}
|
||||
{h.configured
|
||||
? t("settings.downloadAuth.update")
|
||||
: t("settings.downloadAuth.set")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{open && (
|
||||
<div className="mt-2 space-y-2">
|
||||
{/* Write-only: the field is ALWAYS empty on open — the stored blob is never
|
||||
fetched or prefilled. */}
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
rows={5}
|
||||
placeholder={t("settings.downloadAuth.placeholder")}
|
||||
className={`${inputCls} font-mono text-xs resize-y`}
|
||||
data-testid={`cookie-input-${h.host_key}`}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="text-2xs text-muted leading-relaxed">
|
||||
{t("settings.downloadAuth.help")}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={!draft.trim() || save.isPending}
|
||||
onClick={() => save.mutate({ host: h.host_key, cookies: draft })}
|
||||
>
|
||||
{save.isPending
|
||||
? t("settings.downloadAuth.saving")
|
||||
: t("settings.downloadAuth.save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setOpenHost(null);
|
||||
setDraft("");
|
||||
}}
|
||||
>
|
||||
{t("settings.downloadAuth.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user