mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat(dirty-tracking): implement dirty tracking and unsaved changes modal for media settings and naming sections
This commit is contained in:
@@ -16,8 +16,9 @@
|
||||
export let arrType: 'radarr' | 'sonarr';
|
||||
export let canWriteToBase: boolean = false;
|
||||
|
||||
// Edit mode state
|
||||
let isEditing = false;
|
||||
// Edit mode state (exported for parent dirty tracking)
|
||||
export let isEditing = false;
|
||||
export let hasChanges = false;
|
||||
let isSaving = false;
|
||||
|
||||
// Layer selection
|
||||
@@ -29,6 +30,15 @@
|
||||
let formPropersRepacks: PropersRepacks = settings?.propers_repacks ?? 'doNotPrefer';
|
||||
let formEnableMediaInfo: boolean = settings?.enable_media_info ?? true;
|
||||
|
||||
// Original values for dirty tracking
|
||||
let originalPropersRepacks: PropersRepacks = 'doNotPrefer';
|
||||
let originalEnableMediaInfo: boolean = true;
|
||||
|
||||
// Dirty tracking
|
||||
$: hasChanges =
|
||||
formPropersRepacks !== originalPropersRepacks ||
|
||||
formEnableMediaInfo !== originalEnableMediaInfo;
|
||||
|
||||
// Reset form to current settings
|
||||
function resetForm() {
|
||||
formPropersRepacks = settings?.propers_repacks ?? 'doNotPrefer';
|
||||
@@ -37,6 +47,9 @@
|
||||
|
||||
function startEditing() {
|
||||
resetForm();
|
||||
// Save original values for dirty tracking
|
||||
originalPropersRepacks = settings?.propers_repacks ?? 'doNotPrefer';
|
||||
originalEnableMediaInfo = settings?.enable_media_info ?? true;
|
||||
isEditing = true;
|
||||
}
|
||||
|
||||
@@ -187,8 +200,8 @@
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleSaveClick}
|
||||
disabled={isSaving}
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-700 disabled:opacity-50 dark:bg-accent-500 dark:hover:bg-accent-600"
|
||||
disabled={isSaving || !hasChanges}
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent-500 dark:hover:bg-accent-600"
|
||||
>
|
||||
{#if isSaving}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
export let arrType: 'radarr' | 'sonarr';
|
||||
export let canWriteToBase: boolean = false;
|
||||
|
||||
// Edit mode state
|
||||
let isEditing = false;
|
||||
// Edit mode state (exported for parent dirty tracking)
|
||||
export let isEditing = false;
|
||||
export let hasChanges = false;
|
||||
let isSaving = false;
|
||||
|
||||
// Layer selection
|
||||
@@ -60,6 +61,41 @@
|
||||
let formMovieFormat: string = '';
|
||||
let formMovieFolderFormat: string = '';
|
||||
|
||||
// Original values for dirty tracking
|
||||
let originalRename: boolean = false;
|
||||
let originalReplaceIllegalCharacters: boolean = false;
|
||||
let originalColonReplacement: ColonReplacementFormat = 'delete';
|
||||
let originalCustomColonReplacement: string = '';
|
||||
let originalStandardEpisodeFormat: string = '';
|
||||
let originalDailyEpisodeFormat: string = '';
|
||||
let originalAnimeEpisodeFormat: string = '';
|
||||
let originalSeriesFolderFormat: string = '';
|
||||
let originalSeasonFolderFormat: string = '';
|
||||
let originalMultiEpisodeStyle: MultiEpisodeStyle = 'extend';
|
||||
let originalRadarrColonReplacement: RadarrColonReplacementFormat = 'delete';
|
||||
let originalMovieFormat: string = '';
|
||||
let originalMovieFolderFormat: string = '';
|
||||
|
||||
// Dirty tracking
|
||||
$: hasChanges = naming
|
||||
? isSonarrNaming(naming)
|
||||
? formRename !== originalRename ||
|
||||
formReplaceIllegalCharacters !== originalReplaceIllegalCharacters ||
|
||||
formColonReplacement !== originalColonReplacement ||
|
||||
formCustomColonReplacement !== originalCustomColonReplacement ||
|
||||
formStandardEpisodeFormat !== originalStandardEpisodeFormat ||
|
||||
formDailyEpisodeFormat !== originalDailyEpisodeFormat ||
|
||||
formAnimeEpisodeFormat !== originalAnimeEpisodeFormat ||
|
||||
formSeriesFolderFormat !== originalSeriesFolderFormat ||
|
||||
formSeasonFolderFormat !== originalSeasonFolderFormat ||
|
||||
formMultiEpisodeStyle !== originalMultiEpisodeStyle
|
||||
: formRename !== originalRename ||
|
||||
formReplaceIllegalCharacters !== originalReplaceIllegalCharacters ||
|
||||
formRadarrColonReplacement !== originalRadarrColonReplacement ||
|
||||
formMovieFormat !== originalMovieFormat ||
|
||||
formMovieFolderFormat !== originalMovieFolderFormat
|
||||
: false;
|
||||
|
||||
// Reset form to current settings
|
||||
function resetForm() {
|
||||
if (naming && isSonarrNaming(naming)) {
|
||||
@@ -84,6 +120,25 @@
|
||||
|
||||
function startEditing() {
|
||||
resetForm();
|
||||
// Save original values for dirty tracking
|
||||
if (naming && isSonarrNaming(naming)) {
|
||||
originalRename = naming.rename;
|
||||
originalReplaceIllegalCharacters = naming.replace_illegal_characters;
|
||||
originalColonReplacement = naming.colon_replacement_format;
|
||||
originalCustomColonReplacement = naming.custom_colon_replacement_format ?? '';
|
||||
originalStandardEpisodeFormat = naming.standard_episode_format;
|
||||
originalDailyEpisodeFormat = naming.daily_episode_format;
|
||||
originalAnimeEpisodeFormat = naming.anime_episode_format;
|
||||
originalSeriesFolderFormat = naming.series_folder_format;
|
||||
originalSeasonFolderFormat = naming.season_folder_format;
|
||||
originalMultiEpisodeStyle = naming.multi_episode_style;
|
||||
} else if (naming && isRadarrNaming(naming)) {
|
||||
originalRename = naming.rename;
|
||||
originalReplaceIllegalCharacters = naming.replace_illegal_characters;
|
||||
originalRadarrColonReplacement = naming.colon_replacement_format;
|
||||
originalMovieFormat = naming.movie_format;
|
||||
originalMovieFolderFormat = naming.movie_folder_format;
|
||||
}
|
||||
isEditing = true;
|
||||
}
|
||||
|
||||
@@ -359,8 +414,8 @@
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleSaveClick}
|
||||
disabled={isSaving}
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-700 disabled:opacity-50 dark:bg-accent-500 dark:hover:bg-accent-600"
|
||||
disabled={isSaving || !hasChanges}
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent-500 dark:hover:bg-accent-600"
|
||||
>
|
||||
{#if isSaving}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
@@ -512,8 +567,8 @@
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleSaveClick}
|
||||
disabled={isSaving}
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-700 disabled:opacity-50 dark:bg-accent-500 dark:hover:bg-accent-600"
|
||||
disabled={isSaving || !hasChanges}
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-accent-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent-500 dark:hover:bg-accent-600"
|
||||
>
|
||||
{#if isSaving}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
|
||||
@@ -23,8 +23,9 @@
|
||||
export let arrType: 'radarr' | 'sonarr';
|
||||
export let canWriteToBase: boolean = false;
|
||||
|
||||
// Edit mode state
|
||||
let isEditing = false;
|
||||
// Edit mode state (exported for parent dirty tracking)
|
||||
export let isEditing = false;
|
||||
export let hasChanges = false;
|
||||
let isSaving = false;
|
||||
|
||||
// Shared expanded state between read-only and edit mode tables
|
||||
@@ -365,7 +366,7 @@
|
||||
min={0}
|
||||
max={markers[1].value}
|
||||
step={1}
|
||||
on:input={() => syncToDefinition(def.quality_id)}
|
||||
onchange={() => syncToDefinition(def.quality_id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -382,7 +383,7 @@
|
||||
min={markers[0].value}
|
||||
max={markers[2].value}
|
||||
step={1}
|
||||
on:input={() => syncToDefinition(def.quality_id)}
|
||||
onchange={() => syncToDefinition(def.quality_id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -398,7 +399,7 @@
|
||||
bind:value={markers[2].value}
|
||||
min={markers[1].value}
|
||||
step={1}
|
||||
on:input={() => syncToDefinition(def.quality_id)}
|
||||
onchange={() => syncToDefinition(def.quality_id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate, goto } from '$app/navigation';
|
||||
import QualityDefinitionsSection from '../components/QualityDefinitionsSection.svelte';
|
||||
import NamingSection from '../components/NamingSection.svelte';
|
||||
import MediaSettingsSection from '../components/MediaSettingsSection.svelte';
|
||||
import Modal from '$ui/modal/Modal.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
@@ -10,6 +12,49 @@
|
||||
data.mediaManagement.qualityDefinitions.length > 0 ||
|
||||
data.mediaManagement.naming !== null ||
|
||||
data.mediaManagement.mediaSettings !== null;
|
||||
|
||||
// Dirty tracking state from child sections
|
||||
let qualityIsEditing = false;
|
||||
let qualityHasChanges = false;
|
||||
let namingIsEditing = false;
|
||||
let namingHasChanges = false;
|
||||
let mediaIsEditing = false;
|
||||
let mediaHasChanges = false;
|
||||
|
||||
// Aggregate dirty state
|
||||
$: pageIsDirty =
|
||||
(qualityIsEditing && qualityHasChanges) ||
|
||||
(namingIsEditing && namingHasChanges) ||
|
||||
(mediaIsEditing && mediaHasChanges);
|
||||
|
||||
// Navigation blocking
|
||||
let showDirtyModal = false;
|
||||
let pendingNavigationUrl: string | null = null;
|
||||
|
||||
beforeNavigate((navigation) => {
|
||||
if (pageIsDirty) {
|
||||
navigation.cancel();
|
||||
pendingNavigationUrl = navigation.to?.url.pathname || null;
|
||||
showDirtyModal = true;
|
||||
}
|
||||
});
|
||||
|
||||
function handleDiscardChanges() {
|
||||
showDirtyModal = false;
|
||||
if (pendingNavigationUrl) {
|
||||
// Reset all editing states before navigating
|
||||
qualityIsEditing = false;
|
||||
namingIsEditing = false;
|
||||
mediaIsEditing = false;
|
||||
goto(pendingNavigationUrl);
|
||||
}
|
||||
pendingNavigationUrl = null;
|
||||
}
|
||||
|
||||
function handleStayOnPage() {
|
||||
showDirtyModal = false;
|
||||
pendingNavigationUrl = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-8">
|
||||
@@ -26,18 +71,35 @@
|
||||
definitions={data.mediaManagement.qualityDefinitions}
|
||||
arrType="radarr"
|
||||
canWriteToBase={data.canWriteToBase}
|
||||
bind:isEditing={qualityIsEditing}
|
||||
bind:hasChanges={qualityHasChanges}
|
||||
/>
|
||||
|
||||
<NamingSection
|
||||
naming={data.mediaManagement.naming}
|
||||
arrType="radarr"
|
||||
canWriteToBase={data.canWriteToBase}
|
||||
bind:isEditing={namingIsEditing}
|
||||
bind:hasChanges={namingHasChanges}
|
||||
/>
|
||||
|
||||
<MediaSettingsSection
|
||||
settings={data.mediaManagement.mediaSettings}
|
||||
arrType="radarr"
|
||||
canWriteToBase={data.canWriteToBase}
|
||||
bind:isEditing={mediaIsEditing}
|
||||
bind:hasChanges={mediaHasChanges}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={showDirtyModal}
|
||||
header="Unsaved Changes"
|
||||
bodyMessage="You have unsaved changes. Are you sure you want to leave this page? Your changes will be lost."
|
||||
confirmText="Discard Changes"
|
||||
cancelText="Stay on Page"
|
||||
confirmDanger={true}
|
||||
on:confirm={handleDiscardChanges}
|
||||
on:cancel={handleStayOnPage}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate, goto } from '$app/navigation';
|
||||
import QualityDefinitionsSection from '../components/QualityDefinitionsSection.svelte';
|
||||
import NamingSection from '../components/NamingSection.svelte';
|
||||
import MediaSettingsSection from '../components/MediaSettingsSection.svelte';
|
||||
import Modal from '$ui/modal/Modal.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
@@ -10,6 +12,49 @@
|
||||
data.mediaManagement.qualityDefinitions.length > 0 ||
|
||||
data.mediaManagement.naming !== null ||
|
||||
data.mediaManagement.mediaSettings !== null;
|
||||
|
||||
// Dirty tracking state from child sections
|
||||
let qualityIsEditing = false;
|
||||
let qualityHasChanges = false;
|
||||
let namingIsEditing = false;
|
||||
let namingHasChanges = false;
|
||||
let mediaIsEditing = false;
|
||||
let mediaHasChanges = false;
|
||||
|
||||
// Aggregate dirty state
|
||||
$: pageIsDirty =
|
||||
(qualityIsEditing && qualityHasChanges) ||
|
||||
(namingIsEditing && namingHasChanges) ||
|
||||
(mediaIsEditing && mediaHasChanges);
|
||||
|
||||
// Navigation blocking
|
||||
let showDirtyModal = false;
|
||||
let pendingNavigationUrl: string | null = null;
|
||||
|
||||
beforeNavigate((navigation) => {
|
||||
if (pageIsDirty) {
|
||||
navigation.cancel();
|
||||
pendingNavigationUrl = navigation.to?.url.pathname || null;
|
||||
showDirtyModal = true;
|
||||
}
|
||||
});
|
||||
|
||||
function handleDiscardChanges() {
|
||||
showDirtyModal = false;
|
||||
if (pendingNavigationUrl) {
|
||||
// Reset all editing states before navigating
|
||||
qualityIsEditing = false;
|
||||
namingIsEditing = false;
|
||||
mediaIsEditing = false;
|
||||
goto(pendingNavigationUrl);
|
||||
}
|
||||
pendingNavigationUrl = null;
|
||||
}
|
||||
|
||||
function handleStayOnPage() {
|
||||
showDirtyModal = false;
|
||||
pendingNavigationUrl = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-8">
|
||||
@@ -26,18 +71,35 @@
|
||||
definitions={data.mediaManagement.qualityDefinitions}
|
||||
arrType="sonarr"
|
||||
canWriteToBase={data.canWriteToBase}
|
||||
bind:isEditing={qualityIsEditing}
|
||||
bind:hasChanges={qualityHasChanges}
|
||||
/>
|
||||
|
||||
<NamingSection
|
||||
naming={data.mediaManagement.naming}
|
||||
arrType="sonarr"
|
||||
canWriteToBase={data.canWriteToBase}
|
||||
bind:isEditing={namingIsEditing}
|
||||
bind:hasChanges={namingHasChanges}
|
||||
/>
|
||||
|
||||
<MediaSettingsSection
|
||||
settings={data.mediaManagement.mediaSettings}
|
||||
arrType="sonarr"
|
||||
canWriteToBase={data.canWriteToBase}
|
||||
bind:isEditing={mediaIsEditing}
|
||||
bind:hasChanges={mediaHasChanges}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={showDirtyModal}
|
||||
header="Unsaved Changes"
|
||||
bodyMessage="You have unsaved changes. Are you sure you want to leave this page? Your changes will be lost."
|
||||
confirmText="Discard Changes"
|
||||
cancelText="Stay on Page"
|
||||
confirmDanger={true}
|
||||
on:confirm={handleDiscardChanges}
|
||||
on:cancel={handleStayOnPage}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user