improve instance settings drawer UX (#8002)

* fix(frontend): prevent false dirty state in instance settings on load

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): handle undefined python version in select binding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(frontend): extract SaveButton component and improve drawer header UX

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(frontend): replace inline diff with diff drawer and simplify save flow

Save now saves immediately instead of requiring a two-step confirm flow.
Diff view opens in a separate drawer with split/unified toggle instead of
replacing the form content inline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): preserve dirty state when toggling YAML mode in instance settings

syncFormToYaml() was setting yamlCodeInitial to the current modified YAML,
causing hasUnsavedChanges to become false when entering YAML mode with
pending form changes. Build yamlCodeInitial from initialValues instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): clear dirty state after saving in YAML mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* reduce save button timeout

* feat(frontend): add review changes button to unsaved changes confirmation modal

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(frontend): address code review issues from PR #8002

Remove unnecessary IIFE wrappers in handleSave/handleSaveAndCloseDiff,
fix stale on:close reference on diff drawer, clip SaveButton overlay with
overflow-hidden, make DiffEditor respond reactively to inlineDiff prop
instead of using {#key} destroy/recreate, and revert normalizeValue
object check to original simpler behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): remove tab-switch confirmation modal in full settings mode

In full mode, the save button saves all settings across all categories,
so switching tabs cannot lose unsaved changes. Remove the per-category
dirty check, confirmation modal, and unused ConfirmationModal import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): prevent SMTP toggles from creating false dirty state

Use getter/setter bind:checked so Toggle reads undefined as false
without writing it back to the store. This prevents visiting the SMTP
tab from mutating smtp_settings and triggering a false unsaved diff.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): prevent OTEL toggles from creating false dirty state

Same fix as SMTP toggles: use getter/setter bind:checked so Toggle
reads undefined as false without writing it back to the store.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(frontend): use recursive normalizeValue for dirty state instead of per-component fixes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(frontend): replace save button with always-visible review changes button

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(frontend): address PR review comments on DiffEditor and SaveButton

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Guilhem
2026-02-18 21:12:20 +00:00
committed by GitHub
parent 1f3ceb68bb
commit 0ff65550f7
9 changed files with 251 additions and 241 deletions
@@ -169,12 +169,6 @@
open = false
}
function onWidthChange(editorWidth: number) {
diffEditor?.updateOptions({
renderSideBySide: inlineDiff ? false : editorWidth >= SIDE_BY_SIDE_MIN_WIDTH
})
}
$effect(() => {
if (open && diffDivEl) {
loadDiffEditor()
@@ -182,7 +176,11 @@
})
$effect(() => {
onWidthChange(editorWidth)
if (diffEditor) {
diffEditor.updateOptions({
renderSideBySide: inlineDiff ? false : editorWidth >= SIDE_BY_SIDE_MIN_WIDTH
})
}
})
onMount(() => {
@@ -208,7 +208,9 @@
ee_only={setting.ee_only}
settingKey={setting.key}
>
<ToggleButtonGroup bind:selected={$values[setting.key]}>
<ToggleButtonGroup
bind:selected={() => $values[setting.key] ?? 'default', (v) => ($values[setting.key] = v)}
>
{#snippet children({ item: toggleButton })}
{#each setting.select_items ?? [] as item}
<ToggleButton
@@ -548,7 +550,7 @@
<Toggle
disabled
id="metrics_enabled"
bind:checked={$values[setting.key].logs_enabled}
bind:checked={$values[setting.key].metrics_enabled}
options={{ right: 'Metrics (coming soon)' }}
/>
</div>
@@ -30,7 +30,6 @@
onNavigateToTab?: (category: string) => void
quickSetup?: boolean
yamlMode?: boolean
diffMode?: boolean
hasUnsavedChanges?: boolean
}
@@ -42,7 +41,6 @@
onNavigateToTab,
quickSetup = false,
yamlMode = $bindable(false),
diffMode = $bindable(false),
hasUnsavedChanges = $bindable(false)
}: Props = $props()
@@ -86,7 +84,8 @@
function applyFormDefaults(vals: Record<string, any>): void {
for (const [key, defaultVal] of Object.entries(formDefaults)) {
if (vals[key] == undefined) {
vals[key] = typeof defaultVal === 'object' ? JSON.parse(JSON.stringify(defaultVal)) : defaultVal
vals[key] =
typeof defaultVal === 'object' ? JSON.parse(JSON.stringify(defaultVal)) : defaultVal
}
}
}
@@ -111,6 +110,19 @@
}
applyFormDefaults(nvalues)
// Apply select/select_python defaults so initialValues matches what InstanceSetting's $effect does
for (const category of settingsKeys) {
for (const s of settings[category]) {
if (
(s.fieldType === 'select' || s.fieldType === 'select_python') &&
nvalues[s.key] == undefined &&
s.defaultValue
) {
nvalues[s.key] = s.defaultValue()
}
}
}
// Snapshot initialValues before applying the base_url fallback so that
// the dirty-check detects the unsaved default and enables the Save button.
initialValues = JSON.parse(JSON.stringify(nvalues))
@@ -201,6 +213,10 @@
initialRequirePreexistingUserForOauth = requirePreexistingUserForOauth
baseUrlIsFallback = false
if (yamlMode) {
yamlCodeInitial = yamlCode
}
if (licenseKeySet) {
setLicense()
}
@@ -333,11 +349,20 @@
if (value === false) return undefined
if (typeof value === 'string' && value.trim() === '') return undefined
if (Array.isArray(value) && value.length === 0) return undefined
if (typeof value === 'object' && Object.keys(value).length === 0) return undefined
if (typeof value === 'object' && !Array.isArray(value)) {
// Recursively normalize: if all values in the object normalize to undefined,
// the object itself is effectively empty (e.g. {smtp_tls_implicit: false} ≡ {})
const hasNonEmpty = Object.values(value).some((v) => normalizeValue(v) !== undefined)
if (!hasNonEmpty) return undefined
}
// Key-specific defaults: these values are equivalent to "not set"
if (key === 'secret_backend') {
if (typeof value === 'object' && value?.type === 'Database' && Object.keys(value).length === 1) {
if (
typeof value === 'object' &&
value?.type === 'Database' &&
Object.keys(value).length === 1
) {
return undefined
}
}
@@ -658,7 +683,12 @@
normalize: true,
mask: !showSensitive
})
yamlCodeInitial = yamlCode
yamlCodeInitial = buildSettingsYaml(
initialValues,
initialOauths,
initialRequirePreexistingUserForOauth,
{ normalize: true, mask: !showSensitive }
)
yamlEditor?.setCode(yamlCode)
yamlError = ''
}
@@ -797,40 +827,25 @@
</script>
<div class="pb-12">
{#if diffMode}
<div class="w-full h-[calc(100vh-8rem)]">
{#await import('$lib/components/DiffEditor.svelte')}
<Loader2 class="animate-spin m-4" />
{:then Module}
{@const diff = buildFullDiff()}
<Module.default
open={true}
className="!h-full"
defaultLang="yaml"
defaultOriginal={diff.original}
defaultModified={diff.modified}
readOnly
inlineDiff={true}
{#if yamlMode}
<div class="flex flex-row justify-between">
<p class="text-2xs text-tertiary">
Use this YAML to manage instance settings as code.
<a
href="https://www.windmill.dev/docs/advanced/instance_settings#kubernetes-operator"
target="_blank"
rel="noopener noreferrer">Learn more <ExternalLink size={12} class="inline-block" /></a
>
</p>
<!-- svelte-ignore a11y_label_has_associated_control -->
<div class="flex items-center justify-end gap-4 mb-2">
<Toggle
checked={showSensitive}
on:change={(e) => handleShowSensitiveToggle(e.detail)}
options={{ right: 'Show sensitive values' }}
size="xs"
/>
{/await}
</div>
{:else if yamlMode}
<p class="text-2xs text-tertiary mb-2">
Use this YAML to manage instance settings as code.
<a
href="https://www.windmill.dev/docs/advanced/instance_settings#kubernetes-operator"
target="_blank"
rel="noopener noreferrer"
>Learn more <ExternalLink size={12} class="inline-block" /></a>
</p>
<!-- svelte-ignore a11y_label_has_associated_control -->
<div class="flex items-center justify-end gap-4 mb-2">
<Toggle
checked={showSensitive}
on:change={(e) => handleShowSensitiveToggle(e.detail)}
options={{ right: 'Show sensitive values' }}
size="xs"
/>
</div>
</div>
<div class="border rounded w-full h-[calc(100vh-12rem)]">
{#await import('$lib/components/SimpleEditor.svelte')}
@@ -887,11 +902,7 @@
link="https://www.windmill.dev/docs/advanced/imports"
/>
{#if !$enterpriseLicense}
<Alert
type="info"
title="Private registries configuration is an EE feature"
class="mb-2"
/>
<Alert type="info" title="Private registries configuration is an EE feature" class="mb-2" />
{/if}
{:else if category == 'Alerts'}
<SettingsPageHeader
@@ -1026,7 +1037,9 @@
{values}
{version}
{oauths}
warning={setting.key === 'base_url' && baseUrlIsFallback ? 'Auto-detected from browser — not yet saved' : undefined}
warning={setting.key === 'base_url' && baseUrlIsFallback
? 'Auto-detected from browser — not yet saved'
: undefined}
/>
{/if}
{#if quickSetup && category === 'Core' && setting.key === 'base_url'}
@@ -1076,4 +1089,5 @@
/>
{/if}
{/snippet}
</div>
@@ -0,0 +1,99 @@
<script lang="ts">
import { Button } from './common'
import { type ButtonType } from './common/button/model'
import { Save, CheckCircle2, AlertCircle, Loader2 } from 'lucide-svelte'
import { fly } from 'svelte/transition'
let {
onSave,
disabled = false,
label = 'Save settings',
size = undefined,
unifiedSize = undefined,
variant = 'accent'
}: {
onSave: () => void | Promise<void>
disabled?: boolean
label?: string
size?: ButtonType.Size | undefined
unifiedSize?: ButtonType.UnifiedSize | undefined
variant?: ButtonType.Variant
} = $props()
let isSaving = $state(false)
let saveStatus: 'success' | 'error' | null = $state(null)
let statusTimeout: ReturnType<typeof setTimeout> | null = null
function clearStatusTimeout() {
if (statusTimeout !== null) {
clearTimeout(statusTimeout)
statusTimeout = null
}
}
function handleClick() {
if (isSaving) return
isSaving = true
saveStatus = null
clearStatusTimeout()
Promise.resolve(onSave())
.then(() => {
saveStatus = 'success'
statusTimeout = setTimeout(() => {
saveStatus = null
statusTimeout = null
}, 1500)
})
.catch((error) => {
console.error('Save failed:', error)
saveStatus = 'error'
statusTimeout = setTimeout(() => {
saveStatus = null
statusTimeout = null
}, 3000)
})
.finally(() => {
isSaving = false
})
}
$effect(() => {
return () => {
clearStatusTimeout()
}
})
</script>
<div class="relative overflow-hidden rounded-md">
<Button
{variant}
{size}
{unifiedSize}
startIcon={{
icon: isSaving ? Loader2 : Save,
classes: isSaving ? 'animate-spin' : ''
}}
disabled={disabled || isSaving}
onClick={handleClick}
>
{isSaving ? 'Saving...' : label}
</Button>
{#if saveStatus === 'success'}
<div
class="absolute inset-0 flex items-center justify-center bg-green-200 dark:bg-green-800 rounded-md"
transition:fly={{ y: -10, duration: 300 }}
>
<CheckCircle2 class="w-5 h-5 text-green-700 dark:text-green-300" />
</div>
{:else if saveStatus === 'error'}
<div
class="absolute inset-0 flex items-center justify-center bg-red-500 dark:bg-red-700 rounded-md"
transition:fly={{ y: -10, duration: 300 }}
>
<AlertCircle class="w-5 h-5 text-white" />
</div>
{/if}
</div>
@@ -5,7 +5,9 @@
import MeltTooltip from './meltComponents/Tooltip.svelte'
import Toggle from './Toggle.svelte'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import { X, FileDiff, Save, Loader2 } from 'lucide-svelte'
import SaveButton from './SaveButton.svelte'
import { X, FileDiff, Loader2 } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { SettingsService } from '$lib/gen'
import { isCloudHosted } from '$lib/cloud'
@@ -16,14 +18,14 @@
let { disableChatOffset = false }: Props = $props()
let drawer: Drawer | undefined = $state()
let diffDrawer: Drawer | undefined = $state()
let innerComponent: SuperadminSettingsInner | undefined = $state()
let uptodateVersion: string | undefined = $state(undefined)
let yamlMode = $state(false)
let diffMode = $state(false)
let hasUnsavedChanges = $state(false)
let pendingSave = $state(false)
let isSaving = $state(false)
let showCloseConfirmModal = $state(false)
let diffData: { original: string; modified: string } = $state({ original: '', modified: '' })
let inlineDiff = $state(false)
async function loadUptodate() {
try {
@@ -65,39 +67,24 @@
bypassCloseCheck = false
}
async function handleSave() {
if (!pendingSave) {
if (!innerComponent?.syncBeforeDiff()) return
diffMode = true
pendingSave = true
return
}
isSaving = true
try {
await innerComponent?.saveSettings()
diffMode = false
pendingSave = false
} catch (e) {
console.error('Save failed:', e)
} finally {
isSaving = false
}
async function handleSave(): Promise<void> {
if (!innerComponent?.syncBeforeDiff()) throw new Error('YAML sync failed')
await innerComponent?.saveSettings()
}
async function handleSaveAndCloseDiff(): Promise<void> {
await handleSave()
diffDrawer?.closeDrawer()
}
function handleDiscard() {
innerComponent?.discardAll()
diffMode = false
pendingSave = false
}
function handleShowDiff() {
if (!diffMode) {
if (!innerComponent?.syncBeforeDiff()) return
}
diffMode = !diffMode
if (!diffMode) {
pendingSave = false
}
function handleReviewChanges() {
if (!innerComponent?.syncBeforeDiff()) return
diffData = innerComponent?.buildFullDiff() ?? { original: '', modified: '' }
diffDrawer?.openDrawer()
}
</script>
@@ -124,41 +111,32 @@
{/snippet}
{#snippet actions()}
<div class="flex items-center gap-2">
{#if hasUnsavedChanges}
<div transition:fade={{ duration: 150 }}>
<Button
variant="default"
size="xs"
startIcon={{ icon: X }}
onClick={handleDiscard}
>
Discard
</Button>
</div>
{/if}
<Button
variant="default"
size="xs"
startIcon={{ icon: X }}
onClick={handleDiscard}
disabled={!hasUnsavedChanges || isSaving}
>
Discard
</Button>
<Button
variant={diffMode ? 'accent' : 'default'}
variant="accent"
size="xs"
startIcon={{ icon: FileDiff }}
onClick={handleShowDiff}
onClick={handleReviewChanges}
disabled={!hasUnsavedChanges}
>
{diffMode ? 'Hide diff' : 'Show diff'}
Review changes
</Button>
<Toggle
bind:checked={yamlMode}
options={{ right: 'YAML' }}
size="sm"
/>
<Button
variant="accent"
size="xs"
startIcon={{
icon: isSaving ? Loader2 : Save,
classes: isSaving ? 'animate-spin' : ''
}}
disabled={!hasUnsavedChanges || isSaving}
onClick={handleSave}
>
{isSaving ? 'Saving...' : pendingSave ? 'Confirm & Save' : 'Save settings'}
</Button>
</div>
{/snippet}
<SuperadminSettingsInner
@@ -166,12 +144,40 @@
closeDrawer={handleClose}
showHeaderInfo={false}
bind:yamlMode
bind:diffMode
bind:hasUnsavedChanges
/>
</DrawerContent>
</Drawer>
<Drawer bind:this={diffDrawer} size="1200px">
<DrawerContent title="Review changes" on:close={() => diffDrawer?.closeDrawer()}>
{#snippet actions()}
<Toggle
bind:checked={inlineDiff}
options={{ right: 'Unified' }}
size="xs"
/>
<SaveButton onSave={handleSaveAndCloseDiff} disabled={!hasUnsavedChanges} size="xs" />
{/snippet}
<!-- DiffEditor reacts to inlineDiff changes via $effect — no {#key} needed -->
<div class="h-full">
{#await import('$lib/components/DiffEditor.svelte')}
<Loader2 class="animate-spin m-4" />
{:then Module}
<Module.default
open={true}
className="!h-full"
defaultLang="yaml"
defaultOriginal={diffData.original}
defaultModified={diffData.modified}
readOnly
{inlineDiff}
/>
{/await}
</div>
</DrawerContent>
</Drawer>
{#if showCloseConfirmModal}
<ConfirmationModal
open={showCloseConfirmModal}
@@ -183,8 +189,6 @@
on:confirmed={() => {
innerComponent?.discardAll()
showCloseConfirmModal = false
diffMode = false
pendingSave = false
closeDrawer()
}}
>
@@ -192,14 +196,14 @@
<span>You have unsaved changes. Are you sure you want to discard them and close?</span>
<Button
variant="default"
size="xs"
size="sm"
startIcon={{ icon: FileDiff }}
onClick={() => {
showCloseConfirmModal = false
diffMode = true
handleReviewChanges()
}}
>
Show diff
Review changes
</Button>
</div>
</ConfirmationModal>
@@ -46,7 +46,6 @@
closeDrawer,
showHeaderInfo = true,
yamlMode = $bindable(false),
diffMode = $bindable(false),
hasUnsavedChanges = $bindable(false)
} = $props()
@@ -142,6 +141,9 @@
return instanceSettings?.syncBeforeDiff() ?? true
}
export function buildFullDiff(): { original: string; modified: string } {
return instanceSettings?.buildFullDiff() ?? { original: '', modified: '' }
}
// --- Settings search ---
const searchableItems = buildSearchableSettingItems()
@@ -203,7 +205,7 @@
{/if}
{/if}
<div class="{showHeaderInfo ? 'pt-4' : ''} flex grow min-h-0">
{#if !yamlMode && !diffMode}
{#if !yamlMode}
<!-- Sidebar Navigation -->
<div class="w-52 shrink-0 h-full overflow-auto p-4 bg-surface flex flex-col">
<SettingsSearchInput {searchableItems} onSelect={handleSearchSelect} class="mb-3" />
@@ -231,7 +233,7 @@
<div class="flex-1 min-w-0 h-full">
<div class="h-full overflow-auto bg-surface">
<div class="h-fit px-8 py-4">
{#if tab === 'users' && !yamlMode && !diffMode}
{#if tab === 'users' && !yamlMode}
<div class="h-full">
{#if !automateUsernameCreation && !isCloudHosted()}
<div class="mb-4">
@@ -507,7 +509,6 @@
bind:this={instanceSettings}
hideTabs
bind:yamlMode
bind:diffMode
bind:hasUnsavedChanges
tab={instanceSettingsCategory}
{authSubTab}
@@ -543,4 +544,3 @@
<span>Are you sure you want to remove <b>{deleteUserEmail}</b>?</span>
</div>
</ConfirmationModal>
@@ -181,7 +181,7 @@ export const settings: Record<string, Setting[]> = {
],
Jobs: [
{
label: 'Job Isolation',
label: 'Job isolation',
key: 'job_isolation',
fieldType: 'select',
description:
@@ -1,7 +1,8 @@
<script lang="ts">
import { Button } from '../common'
import { Save, X, CheckCircle2, AlertCircle, Loader2 } from 'lucide-svelte'
import { fade, fly } from 'svelte/transition'
import SaveButton from '../SaveButton.svelte'
import { X } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { twMerge } from 'tailwind-merge'
let {
@@ -21,52 +22,6 @@
inline?: boolean
class?: string
} = $props()
let saveStatus: 'success' | 'error' | null = $state(null)
let isSaving = $state(false)
let statusTimeout: ReturnType<typeof setTimeout> | null = null
function clearStatusTimeout() {
if (statusTimeout !== null) {
clearTimeout(statusTimeout)
statusTimeout = null
}
}
async function handleSave() {
if (isSaving) return
isSaving = true
saveStatus = null
clearStatusTimeout() // Clear any existing timeout
try {
await onSave()
saveStatus = 'success'
// Auto-hide success status after 3 seconds
statusTimeout = setTimeout(() => {
saveStatus = null
statusTimeout = null
}, 3000)
} catch (error) {
console.error('Save failed:', error)
saveStatus = 'error'
// Auto-hide error status after 5 seconds (longer for errors)
statusTimeout = setTimeout(() => {
saveStatus = null
statusTimeout = null
}, 5000)
} finally {
isSaving = false
}
}
// Cleanup timeout on component unmount
$effect(() => {
return () => {
clearStatusTimeout()
}
})
</script>
<div
@@ -81,44 +36,18 @@
size="sm"
startIcon={{ icon: X }}
onClick={onDiscard}
disabled={isSaving}
>
Discard changes
</Button>
</div>
{/if}
<div class="relative">
<Button
variant="accent"
unifiedSize="md"
startIcon={{
icon: isSaving ? Loader2 : Save,
classes: isSaving ? 'animate-spin' : ''
}}
disabled={!hasUnsavedChanges || disabled || isSaving}
onClick={handleSave}
>
{isSaving ? 'Saving...' : saveLabel}
</Button>
<!-- Success icon overlay -->
{#if saveStatus === 'success'}
<div
class="absolute inset-0 flex items-center justify-center bg-green-200 dark:bg-green-800 rounded-md"
transition:fly={{ y: -10, duration: 300 }}
>
<CheckCircle2 class="w-5 h-5 text-green-700 dark:text-green-300" />
</div>
{:else if saveStatus === 'error'}
<div
class="absolute inset-0 flex items-center justify-center bg-red-500 dark:bg-red-700 rounded-md"
transition:fly={{ y: -10, duration: 300 }}
>
<AlertCircle class="w-5 h-5 text-white" />
</div>
{/if}
</div>
<SaveButton
{onSave}
disabled={!hasUnsavedChanges || disabled}
label={saveLabel}
unifiedSize="md"
/>
</div>
</div>
</div>
@@ -5,7 +5,6 @@
import InstanceSettings from '$lib/components/InstanceSettings.svelte'
import { Alert, Button } from '$lib/components/common'
import SidebarNavigation from '$lib/components/common/sidebar/SidebarNavigation.svelte'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import {
setupNavigationGroups,
tabToCategoryMap,
@@ -25,6 +24,7 @@
import Toggle from '$lib/components/Toggle.svelte'
import SettingsPageHeader from '$lib/components/settings/SettingsPageHeader.svelte'
import SettingCard from '$lib/components/instanceSettings/SettingCard.svelte'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
const settingsSteps = [
{ id: 'Core', label: 'Core' },
@@ -148,19 +148,9 @@
let authSubTab: 'sso' | 'oauth' | 'scim' = $derived(tabToAuthSubTab[fullTab] ?? 'sso')
let yamlMode = $state(false)
// --- Unsaved changes detection (full mode) ---
let pendingTab: string | undefined = $state(undefined)
let showUnsavedChangesModal = $state(false)
function handleNavigate(newTab: string) {
if (newTab === fullTab) return
const currentCategory = tabToCategoryMap[fullTab]
if (currentCategory && instanceSettings?.isDirty(currentCategory)) {
pendingTab = newTab
showUnsavedChangesModal = true
} else {
fullTab = newTab
}
fullTab = newTab
}
// --- Settings search (full mode) ---
@@ -558,33 +548,6 @@
</div>
</CenteredModal>
{#if showUnsavedChangesModal}
<ConfirmationModal
open={showUnsavedChangesModal}
title="Unsaved changes detected"
confirmationText="Discard changes"
on:canceled={() => {
showUnsavedChangesModal = false
pendingTab = undefined
}}
on:confirmed={() => {
if (pendingTab !== undefined) {
const currentCategory = tabToCategoryMap[fullTab]
if (currentCategory) {
instanceSettings?.discardCategory(currentCategory)
}
fullTab = pendingTab
}
showUnsavedChangesModal = false
pendingTab = undefined
}}
>
<div class="flex flex-col w-full space-y-4">
<span>You have unsaved changes. Are you sure you want to discard them?</span>
</div>
</ConfirmationModal>
{/if}
{#if showLicenseKeyWarning}
<ConfirmationModal
open={showLicenseKeyWarning}
@@ -603,7 +566,8 @@
>
<div class="flex flex-col w-full space-y-4">
<span>
You are running the Enterprise Edition image but have not entered a license key. A valid license key is required to use EE features. Are you sure you want to continue without one?
You are running the Enterprise Edition image but have not entered a license key. A valid
license key is required to use EE features. Are you sure you want to continue without one?
</span>
</div>
</ConfirmationModal>