feat: add input/checkbox to confirm dialog and multi-selection actions
Introduce richer confirmation dialogs and wire them through the UI so actions
can collect an optional single-line input and a checkbox option.
- ConfirmDialog: support optional input and checkbox (with defaultChecked),
expose onConfirm(result: {checked, value}). Focus/selects input when present
and blocks confirm while required inputs/checkboxes are missing.
- App: add askConfirmation(...) returning {confirmed, value, checked} and keep
requestConfirmation(...) as a convenience boolean wrapper. Update answer flow
to pass the full result. Use the new prompt in stashStatusFiles to collect a
stash message and "include untracked" option before saving.
- Status/Explorer/Worktree: add multi-selection support for stop-tracking and
stash operations. onStopTracking now accepts an array of paths and a new
"selection" kind; status context menu shows selection counts and uses a
CopyCheck icon for selections. Added corresponding i18n messages.
This change keeps existing UX but enables collecting extra confirmation data
and acting on multi-file selections.
This commit is contained in:
@@ -19,8 +19,10 @@
|
||||
note?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
/** Optional opt-in the user must tick before confirming, e.g. "delete anyway". */
|
||||
checkbox?: { label: string; note?: string; required?: boolean };
|
||||
/** Optional opt-in, e.g. "delete anyway" (required) or "include untracked". */
|
||||
checkbox?: { label: string; note?: string; required?: boolean; defaultChecked?: boolean };
|
||||
/** Optional single-line input, e.g. a stash message. */
|
||||
input?: { label: string; placeholder?: string; value?: string; optional?: boolean };
|
||||
/** Destructive actions get the red confirm button and warning icon. */
|
||||
danger?: boolean;
|
||||
}
|
||||
@@ -28,8 +30,8 @@
|
||||
interface Props {
|
||||
request: ConfirmRequest;
|
||||
isBusy?: boolean;
|
||||
/** `checked` is the state of the optional checkbox. */
|
||||
onConfirm: (checked: boolean) => void;
|
||||
/** Carries the state of the optional checkbox and input. */
|
||||
onConfirm: (result: { checked: boolean; value: string }) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
@@ -42,18 +44,28 @@
|
||||
let danger = $derived(request.danger !== false);
|
||||
let items = $derived(request.items ?? []);
|
||||
let checked = $state(false);
|
||||
let blocked = $derived(Boolean(request.checkbox?.required) && !checked);
|
||||
let value = $state("");
|
||||
let inputElement = $state<HTMLInputElement | null>(null);
|
||||
let missingInput = $derived(Boolean(request.input) && request.input?.optional !== true && value.trim().length === 0);
|
||||
let blocked = $derived((Boolean(request.checkbox?.required) && !checked) || missingInput);
|
||||
|
||||
$effect(() => {
|
||||
// Reset the opt-in whenever a different confirmation is shown.
|
||||
// Start from the defaults again whenever a different confirmation is shown.
|
||||
request.title;
|
||||
checked = false;
|
||||
checked = request.checkbox?.defaultChecked ?? false;
|
||||
value = request.input?.value ?? "";
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
confirmButton?.focus();
|
||||
// The input is the first thing to fill in when there is one.
|
||||
if (inputElement) inputElement.select();
|
||||
else confirmButton?.focus();
|
||||
});
|
||||
|
||||
function submit() {
|
||||
if (!isBusy && !blocked) onConfirm({ checked, value });
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
@@ -116,6 +128,22 @@
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if request.input}
|
||||
<label class="confirm-input">
|
||||
<span>{request.input.label}</span>
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
bind:value
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder={request.input.placeholder ?? ""}
|
||||
disabled={isBusy}
|
||||
onkeydown={(event) => { if (event.key === "Enter") { event.preventDefault(); submit(); } }}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{#if request.checkbox}
|
||||
<label class="confirm-check">
|
||||
<input type="checkbox" bind:checked disabled={isBusy} />
|
||||
@@ -140,7 +168,7 @@
|
||||
bind:this={confirmButton}
|
||||
class={`confirm-action ${danger ? "btn-danger" : "btn-primary"}`}
|
||||
type="button"
|
||||
onclick={() => onConfirm(checked)}
|
||||
onclick={submit}
|
||||
disabled={isBusy || blocked}
|
||||
>
|
||||
{#if isBusy}
|
||||
@@ -206,6 +234,13 @@
|
||||
background: color-mix(in srgb, var(--color-accent) 9%, transparent);
|
||||
}
|
||||
.confirm-dialog .confirm-action { min-width: 116px; }
|
||||
.confirm-dialog .confirm-input { display: grid; gap: 5px; }
|
||||
.confirm-dialog .confirm-input span {
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 11.5px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.confirm-dialog .confirm-input input { height: 32px; font-size: 12.5px; }
|
||||
.confirm-dialog .confirm-check {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
@@ -217,6 +252,11 @@
|
||||
background: rgba(255, 90, 103, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
.confirm-dialog:not(.danger) .confirm-check {
|
||||
border-color: var(--color-border-subtle);
|
||||
background: color-mix(in srgb, var(--color-accent) 5%, transparent);
|
||||
}
|
||||
.confirm-dialog:not(.danger) .confirm-check input { accent-color: var(--color-accent); }
|
||||
.confirm-dialog .confirm-check input { width: 15px; height: 15px; margin-top: 1px; accent-color: #e86060; }
|
||||
.confirm-dialog .confirm-check span { display: grid; gap: 2px; min-width: 0; }
|
||||
.confirm-dialog .confirm-check strong { color: var(--color-ink); font-size: 12.5px; font-weight: 650; }
|
||||
|
||||
Reference in New Issue
Block a user