mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 14:05:02 -04:00
refactor(webui): unify destructive confirmations
This commit is contained in:
11 files changed
+119
-110
No files matched your search
@@ -308,6 +308,7 @@
|
||||
background: var(--color-panel-raised);
|
||||
color: var(--color-signal);
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
@@ -839,6 +840,10 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar nav a:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar nav a {
|
||||
min-width: 0;
|
||||
min-height: 3.4rem;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog, Dialog } from "bits-ui";
|
||||
import { Dialog } from "bits-ui";
|
||||
import { sources, type ProviderAccount } from "$lib/api";
|
||||
import { audienceLabel } from "$lib/sources";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -133,21 +134,14 @@
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
|
||||
<AlertDialog.Root bind:open={confirmOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay class="dialog-overlay" />
|
||||
<AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>Share this connection with {expansion === "library" ? "a library" : "everyone"}?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
{expansion === "library"
|
||||
? `Every user with access to library ${libraryScopeId} may use this account's supported Source capabilities.`
|
||||
: "Every Allstarr user will be allowed to use this account for its supported Source capabilities."}
|
||||
Credentials remain hidden.
|
||||
</AlertDialog.Description>
|
||||
<footer>
|
||||
<AlertDialog.Cancel class="button-secondary">Keep current access</AlertDialog.Cancel>
|
||||
<AlertDialog.Action class="button-danger" onclick={() => void save()}>Share with {expansion === "library" ? "library" : "everyone"}</AlertDialog.Action>
|
||||
</footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
<ConfirmDialog
|
||||
bind:open={confirmOpen}
|
||||
title={`Share this connection with ${expansion === "library" ? "a library" : "everyone"}?`}
|
||||
description={`${expansion === "library"
|
||||
? `Every user with access to library ${libraryScopeId} may use this account's supported Source capabilities.`
|
||||
: "Every Allstarr user will be allowed to use this account for its supported Source capabilities."} Credentials remain hidden.`}
|
||||
confirmLabel={`Share with ${expansion === "library" ? "library" : "everyone"}`}
|
||||
cancelLabel="Keep current access"
|
||||
disabled={saving}
|
||||
onConfirm={save}
|
||||
/>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog } from "bits-ui";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
title,
|
||||
description,
|
||||
confirmLabel,
|
||||
cancelLabel = "Cancel",
|
||||
confirmClass = "button-danger",
|
||||
disabled = false,
|
||||
onConfirm,
|
||||
}: {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
cancelLabel?: string;
|
||||
confirmClass?: string;
|
||||
disabled?: boolean;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialog.Root bind:open>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay class="dialog-overlay" />
|
||||
<AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>{title}</AlertDialog.Title>
|
||||
<AlertDialog.Description>{description}</AlertDialog.Description>
|
||||
<footer>
|
||||
<AlertDialog.Cancel class="button-secondary" {disabled}>{cancelLabel}</AlertDialog.Cancel>
|
||||
<AlertDialog.Action class={confirmClass} {disabled} onclick={() => void onConfirm()}>{confirmLabel}</AlertDialog.Action>
|
||||
</footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { AlertDialog } from "bits-ui";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
import {
|
||||
downloads,
|
||||
home,
|
||||
@@ -263,23 +263,13 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<AlertDialog.Root bind:open={confirmOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay class="dialog-overlay" />
|
||||
<AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>{removal?.kind === "all" ? `Remove all ${label.toLowerCase()} tracks?` : "Remove this track?"}</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
{removal?.kind === "all"
|
||||
? `This deletes every ${label.toLowerCase()} audio file and its lyrics sidecar.`
|
||||
: "This deletes the managed audio file and its lyrics sidecar. This cannot be undone."}
|
||||
</AlertDialog.Description>
|
||||
<footer>
|
||||
<AlertDialog.Cancel class="button-secondary">Cancel</AlertDialog.Cancel>
|
||||
<AlertDialog.Action class="button-danger" onclick={() => void remove()}>
|
||||
{removal?.kind === "all" ? "Remove all" : "Remove track"}
|
||||
</AlertDialog.Action>
|
||||
</footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
<ConfirmDialog
|
||||
bind:open={confirmOpen}
|
||||
title={removal?.kind === "all" ? `Remove all ${label.toLowerCase()} tracks?` : "Remove this track?"}
|
||||
description={removal?.kind === "all"
|
||||
? `This deletes every ${label.toLowerCase()} audio file and its lyrics sidecar.`
|
||||
: "This deletes the managed audio file and its lyrics sidecar. This cannot be undone."}
|
||||
confirmLabel={removal?.kind === "all" ? "Remove all" : "Remove track"}
|
||||
onConfirm={remove}
|
||||
/>
|
||||
{/if}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog } from "bits-ui";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
import { home, intelligence, type IntelligenceScope, type IntelligenceState } from "$lib/api";
|
||||
|
||||
let protocol = $state("jellyfin");
|
||||
@@ -198,13 +198,14 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<AlertDialog.Root bind:open={purgeOpen}>
|
||||
<AlertDialog.Portal><AlertDialog.Overlay class="dialog-overlay" /><AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>Clear this library’s Intelligence data?</AlertDialog.Title>
|
||||
<AlertDialog.Description>Retained signals, profiles, recommendations, feedback, and generated sets for this exact scope will be removed.</AlertDialog.Description>
|
||||
<footer><AlertDialog.Cancel class="button-secondary">Keep my data</AlertDialog.Cancel><AlertDialog.Action class="button-danger" onclick={() => void perform("purge", () => intelligence.purge(scope))}>Turn off and clear</AlertDialog.Action></footer>
|
||||
</AlertDialog.Content></AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
<ConfirmDialog
|
||||
bind:open={purgeOpen}
|
||||
title="Clear this library’s Intelligence data?"
|
||||
description="Retained signals, profiles, recommendations, feedback, and generated sets for this exact scope will be removed."
|
||||
confirmLabel="Turn off and clear"
|
||||
cancelLabel="Keep my data"
|
||||
onConfirm={() => perform("purge", () => intelligence.purge(scope))}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.intelligence-view{display:grid;gap:1.25rem}.route-heading{display:flex;align-items:end;justify-content:space-between;gap:1rem}.route-heading h2{margin:.25rem 0;font-family:var(--font-display);font-size:clamp(1.5rem,3vw,2.2rem)}.route-heading p:last-child,.muted{color:var(--color-ink-muted)}.heading-actions{display:flex;align-items:center;gap:.75rem}.scope-card{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;align-items:end;gap:1rem;padding:1rem}.field select{min-height:3rem;border:1px solid var(--color-edge);border-radius:.8rem;background:var(--color-panel-raised);color:var(--color-ink);padding:0 .9rem}.run-progress{display:grid;grid-template-columns:minmax(0,1fr) minmax(10rem,.5fr) auto;align-items:center;gap:1rem;padding:1rem}.run-progress p{margin:0}.run-progress small{display:block;color:var(--color-ink-muted)}.run-progress progress{width:100%;accent-color:var(--color-signal)}.intelligence-grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(18rem,.8fr);gap:1rem}.recommendations,.profile-card,.generated-card,.privacy-card{padding:1.15rem}.recommendations>header,.privacy-card>header{display:flex;align-items:center;justify-content:space-between}.recommendations h3,.profile-card h3,.generated-card h3,.privacy-card h3{margin:.2rem 0 1rem}.recommendation-list{display:grid;margin:0;padding:0;list-style:none}.recommendation-list>li{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:.85rem;align-items:center;border-top:1px solid var(--color-edge);padding:.9rem 0}.track-art{display:grid;width:3rem;height:3rem;place-items:center;overflow:hidden;border-radius:.7rem;background:var(--color-panel-raised);color:var(--color-signal)}.track-art img{width:100%;height:100%;object-fit:cover}.track-copy{min-width:0}.track-copy>strong,.track-copy>small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.track-copy small,summary{color:var(--color-ink-muted);font-size:.75rem}.track-copy details{margin-top:.35rem}.track-copy ul{margin:.4rem 0 0;padding-left:1.1rem;color:var(--color-ink-muted);font-size:.78rem}.track-actions{display:flex;align-items:center;gap:.35rem}.track-actions button{border:1px solid var(--color-edge);border-radius:.55rem;background:transparent;color:var(--color-ink-muted);padding:.35rem .5rem;cursor:pointer}.score{color:var(--color-signal);font-size:.75rem;font-weight:800}.side-stack{display:grid;align-content:start;gap:1rem}.profile-card label,.generated-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;border-top:1px solid var(--color-edge);padding:.7rem 0}.profile-card meter{width:55%;accent-color:var(--color-signal)}.generated-row span:first-child strong,.generated-row span:first-child small{display:block}.generated-row small{color:var(--color-ink-muted)}.generate-form{display:grid;gap:.75rem;margin-top:1rem}.privacy-card{grid-column:1/-1}.privacy-card form{display:grid;grid-template-columns:minmax(14rem,.6fr) 1fr 1fr;gap:1rem}.toggle-line{display:flex;gap:.75rem}.toggle-line span>*{display:block}.toggle-line small,fieldset small{color:var(--color-ink-muted)}fieldset{display:grid;align-content:start;gap:.55rem;border:0;margin:0;padding:0}fieldset legend{margin-bottom:.55rem;font-weight:750}fieldset label{display:flex;gap:.5rem}.unavailable{opacity:.5}.privacy-card footer{grid-column:1/-1;display:flex;justify-content:space-between;gap:.75rem;border-top:1px solid var(--color-edge);padding-top:1rem}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { AlertDialog, DropdownMenu } from "bits-ui";
|
||||
import { DropdownMenu } from "bits-ui";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
import {
|
||||
home,
|
||||
matchReview,
|
||||
@@ -426,23 +427,13 @@
|
||||
onReject={(match) => confirm("reject", match)}
|
||||
/>
|
||||
|
||||
<AlertDialog.Root bind:open={destructiveOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay class="dialog-overlay" />
|
||||
<AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>{destructive?.kind === "clear" ? "Clear manual review?" : "Reject this candidate?"}</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
{destructive?.kind === "clear"
|
||||
? "The durable manual decision will be revoked and automatic matching will become authoritative again."
|
||||
: "The current candidate will be recorded as rejected. You can rematch it later."}
|
||||
</AlertDialog.Description>
|
||||
<footer>
|
||||
<AlertDialog.Cancel class="button-secondary">Cancel</AlertDialog.Cancel>
|
||||
<AlertDialog.Action class="button-danger" onclick={() => void applyDestructive()}>
|
||||
{destructive?.kind === "clear" ? "Clear review" : "Reject candidate"}
|
||||
</AlertDialog.Action>
|
||||
</footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
<ConfirmDialog
|
||||
bind:open={destructiveOpen}
|
||||
title={destructive?.kind === "clear" ? "Clear manual review?" : "Reject this candidate?"}
|
||||
description={destructive?.kind === "clear"
|
||||
? "The durable manual decision will be revoked and automatic matching will become authoritative again."
|
||||
: "The current candidate will be recorded as rejected. You can rematch it later."}
|
||||
confirmLabel={destructive?.kind === "clear" ? "Clear review" : "Reject candidate"}
|
||||
onConfirm={applyDestructive}
|
||||
/>
|
||||
{/if}
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { AlertDialog } from "bits-ui";
|
||||
import { home, type JobResponse } from "$lib/api";
|
||||
import { compactProgress, progressDetails } from "$lib/jobs";
|
||||
import { liveUpdates } from "$lib/live-updates.svelte";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
|
||||
let {
|
||||
playlistName,
|
||||
@@ -125,17 +125,13 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<AlertDialog.Root bind:open={cancelOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay class="dialog-overlay" />
|
||||
<AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>Cancel this operation?</AlertDialog.Title>
|
||||
<AlertDialog.Description>Completed durable work remains recorded. The worker will stop at its next safe cancellation point.</AlertDialog.Description>
|
||||
<footer>
|
||||
<AlertDialog.Cancel class="button-secondary">Keep running</AlertDialog.Cancel>
|
||||
<AlertDialog.Action class="button-danger" disabled={cancelling} onclick={() => void cancel()}>Cancel operation</AlertDialog.Action>
|
||||
</footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
<ConfirmDialog
|
||||
bind:open={cancelOpen}
|
||||
title="Cancel this operation?"
|
||||
description="Completed durable work remains recorded. The worker will stop at its next safe cancellation point."
|
||||
confirmLabel="Cancel operation"
|
||||
cancelLabel="Keep running"
|
||||
disabled={cancelling}
|
||||
onConfirm={cancel}
|
||||
/>
|
||||
{/if}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { AlertDialog } from "bits-ui";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
import {
|
||||
home,
|
||||
settings,
|
||||
@@ -338,14 +338,11 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<AlertDialog.Root bind:open={purgeOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay class="dialog-overlay" />
|
||||
<AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>Purge the application cache?</AlertDialog.Title>
|
||||
<AlertDialog.Description>Disposable metadata and media payloads will be removed. PostgreSQL business state, accounts, mappings, playlists, and kept audio are not affected.</AlertDialog.Description>
|
||||
<footer><AlertDialog.Cancel class="button-secondary">Cancel</AlertDialog.Cancel><AlertDialog.Action class="button-danger" onclick={() => void purge()}>Purge cache</AlertDialog.Action></footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
<ConfirmDialog
|
||||
bind:open={purgeOpen}
|
||||
title="Purge the application cache?"
|
||||
description="Disposable metadata and media payloads will be removed. PostgreSQL business state, accounts, mappings, playlists, and kept audio are not affected."
|
||||
confirmLabel="Purge cache"
|
||||
onConfirm={purge}
|
||||
/>
|
||||
{/if}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { AlertDialog, DropdownMenu } from "bits-ui";
|
||||
import { DropdownMenu } from "bits-ui";
|
||||
import ConfirmDialog from "$lib/components/ConfirmDialog.svelte";
|
||||
import {
|
||||
home,
|
||||
sources,
|
||||
@@ -384,19 +385,11 @@
|
||||
<ConnectSourceDialog bind:open={configureOpen} {providers} {administrator} account={selectedAccount} onSaved={completed} />
|
||||
<AccountAccessDialog bind:open={accessOpen} account={selectedAccount} users={audienceUsers} onSaved={completed} />
|
||||
|
||||
<AlertDialog.Root bind:open={removeOpen}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay class="dialog-overlay" />
|
||||
<AlertDialog.Content class="confirm-dialog">
|
||||
<AlertDialog.Title>Remove this Source connection?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
The encrypted credential is revoked and this account can no longer route provider requests. Audit history remains.
|
||||
</AlertDialog.Description>
|
||||
<footer>
|
||||
<AlertDialog.Cancel class="button-secondary">Cancel</AlertDialog.Cancel>
|
||||
<AlertDialog.Action class="button-danger" onclick={() => void remove()}>Remove connection</AlertDialog.Action>
|
||||
</footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Portal>
|
||||
</AlertDialog.Root>
|
||||
<ConfirmDialog
|
||||
bind:open={removeOpen}
|
||||
title="Remove this Source connection?"
|
||||
description="The encrypted credential is revoked and this account can no longer route provider requests. Audit history remains."
|
||||
confirmLabel="Remove connection"
|
||||
onConfirm={remove}
|
||||
/>
|
||||
{/if}
|
||||
@@ -260,7 +260,7 @@
|
||||
</nav>
|
||||
|
||||
<div class="profile">
|
||||
<div class="avatar">
|
||||
<a class="avatar" href="#/settings" aria-label={`Settings for ${session.user?.name ?? "current user"}`}>
|
||||
{#if session.user?.avatarUrl && !avatarFailed}
|
||||
<img
|
||||
src={session.user.avatarUrl}
|
||||
@@ -272,7 +272,7 @@
|
||||
{:else}
|
||||
<span>{initials}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
<div class="min-w-0">
|
||||
<strong>{session.user?.name}</strong>
|
||||
<small>{session.backend}</small>
|
||||
|
||||
@@ -1020,6 +1020,11 @@ test("Profile artwork is stable in full, slim, and mobile navigation", async ({
|
||||
await expect(avatar).toBeVisible();
|
||||
await expect(avatar.locator("img")).toBeVisible();
|
||||
await expect.poll(async () => (await avatar.boundingBox())?.width ?? 0).toBe(40);
|
||||
if (width === 390) {
|
||||
await expect(page.getByRole("navigation", { name: "Primary" }).getByRole("link", { name: "Settings" })).toBeHidden();
|
||||
await expect(avatar).toHaveAttribute("href", "#/settings");
|
||||
await expect.poll(async () => (await page.locator(".sidebar").boundingBox())?.height ?? 0).toBeLessThan(80);
|
||||
}
|
||||
}
|
||||
|
||||
await page.route("**/api/admin/auth/me/avatar?user=user", (route) => route.fulfill({ status: 404 }));
|
||||
|
||||
Reference in new issue
Block a user