feat(frontend): Discard changes confirmation modal (#653)

* feat(front): add a confirmation modal to discard unsaved changes

* feat(front):Fix modal content

* feat(front): Revert code + add unsavedconfirmatiomodal to the flow builder

* feat(front): Use a store to store whether the content of an editor needs to be saved before leaving

* feat(front): Add cleanup + only display the warning when navigating elsewhere on the app

* feat(front): set dirty to false when saving

* feat(front): add keyboard binding to cancel or resume

* feat(front): Init dirtyStore to true when creating a flow/script

* feat(front): Explicit store initiliation

* feat(front): initilisation dirty status after loading a script/flow
This commit is contained in:
Faton Ramadani
2022-10-03 14:32:06 +02:00
committed by GitHub
parent 6a28b68dbf
commit 0e23d2d604
11 changed files with 95 additions and 2 deletions
@@ -39,6 +39,7 @@
langToExt,
updateOptions
} from '$lib/editorUtils'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
try {
StandaloneServices.initialize({
@@ -366,6 +367,8 @@
let timeoutModel: NodeJS.Timeout | undefined = undefined
editor.onDidChangeModelContent((event) => {
$dirtyStore = true
timeoutModel && clearTimeout(timeoutModel)
timeoutModel = setTimeout(() => {
code = getCode()
@@ -16,6 +16,8 @@
import { writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import Button from './common/button/Button.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { OFFSET } from './CronInput.svelte'
import FlowEditor from './flows/FlowEditor.svelte'
import { flowStateStore } from './flows/flowState'
@@ -48,6 +50,7 @@
async function saveFlow(): Promise<void> {
const flow = cleanInputs($flowStore)
const { cron, args, enabled } = $scheduleStore
$dirtyStore = false
if (initialPath === '') {
await FlowService.createFlow({
@@ -155,6 +158,8 @@
loadHubScripts()
</script>
<UnsavedConfirmationModal />
<div class="flex flex-col flex-1 h-full">
<!-- Nav between steps-->
<div class="justify-between flex flex-row w-full my-2 px-4 space-x-4 h-10">
@@ -16,6 +16,8 @@
import ScriptSchema from './ScriptSchema.svelte'
import CenteredPage from './CenteredPage.svelte'
import Tooltip from './Tooltip.svelte'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Button } from './common'
export let script: Script
@@ -41,6 +43,7 @@
async function editScript(): Promise<void> {
try {
$dirtyStore = false
if (!script.schema) {
await inferArgs(script.language, script.content, script.schema)
}
@@ -73,6 +76,7 @@
}
</script>
<UnsavedConfirmationModal />
<div class="flex flex-col h-screen">
<!-- Nav between steps-->
<div class="flex flex-col w-full px-4 py-2 border-b shadow-sm">
@@ -24,6 +24,7 @@
import Button from './common/button/Button.svelte'
import TestJobLoader from './TestJobLoader.svelte'
import { onMount } from 'svelte'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
// Exported
export let schema: Schema = emptySchema()
@@ -5,6 +5,7 @@
import Icon from 'svelte-awesome'
import { fade } from 'svelte/transition'
import Button from '../button/Button.svelte'
import Badge from '../badge/Badge.svelte'
export let title: string
export let confirmationText: string
@@ -12,8 +13,23 @@
export let open: boolean = false
const dispatch = createEventDispatcher()
function onKeyDown(event: KeyboardEvent) {
if (open) {
switch (event.key) {
case 'Enter':
dispatch('confirmed')
break
case 'Escape':
dispatch('canceled')
break
}
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if open}
<div transition:fade={{ duration: 100 }} class={'relative z-50'} role="dialog">
<div
@@ -48,9 +64,11 @@
</div>
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
<Button on:click={() => dispatch('confirmed')} color="red" size="sm">
{confirmationText}
<span>{confirmationText} <Badge>Enter</Badge></span>
</Button>
<Button on:click={() => dispatch('canceled')} color="light" size="sm">
<span>Cancel <Badge color="dark-gray">Escape</Badge></span>
</Button>
<Button on:click={() => dispatch('canceled')} color="light" size="sm">Cancel</Button>
</div>
</div>
</div>
@@ -0,0 +1,47 @@
<script lang="ts">
import ConfirmationModal from './ConfirmationModal.svelte'
import { beforeNavigate, goto } from '$app/navigation'
import { onDestroy } from 'svelte/internal'
import { dirtyStore } from './dirtyStore'
let navigationState: { from: URL; to: URL | null; cancel: () => void } | undefined = undefined
$: open = Boolean(navigationState)
beforeNavigate((newNavigationState) => {
if (
!navigationState &&
$dirtyStore &&
newNavigationState.to?.pathname !== newNavigationState.from.pathname
) {
navigationState = newNavigationState
newNavigationState.cancel()
}
})
onDestroy(() => {
$dirtyStore = false
})
</script>
<ConfirmationModal
{open}
title="Unsaved changes detected"
confirmationText="Discard changes"
on:canceled={() => {
if (navigationState) {
navigationState.cancel()
}
navigationState = undefined
}}
on:confirmed={() => {
if (navigationState?.to) {
goto(navigationState.to)
}
$dirtyStore = false
navigationState = undefined
}}
>
<div class="flex flex-col w-full space-y-4">
<span>Are you sure you want to discard change you have made?</span>
</div>
</ConfirmationModal>
@@ -0,0 +1,3 @@
import { writable } from 'svelte/store'
export const dirtyStore = writable<boolean>(false)
+3
View File
@@ -8,6 +8,7 @@
<script lang="ts">
import { page } from '$app/stores'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { initFlow } from '$lib/components/flows/flowStore'
@@ -55,6 +56,8 @@
}
loadFlow()
$dirtyStore = true
</script>
<FlowBuilder />
@@ -14,6 +14,7 @@
import { workspaceStore } from '$lib/stores'
import { decodeState, emptySchema } from '$lib/utils'
import { initFlow } from '$lib/components/flows/flowStore'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
const initialState = $page.url.searchParams.get('state')
let flowLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
@@ -44,6 +45,7 @@
initialPath = flow.path
initFlow(flow)
$dirtyStore = false
}
$: {
@@ -51,6 +53,8 @@
loadFlow()
}
}
</script>
<FlowBuilder {initialPath} />
+2
View File
@@ -14,6 +14,7 @@
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
import type { Schema } from '$lib/common'
import { decodeState, emptySchema, getScriptByPath, sendUserToast } from '$lib/utils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
// Default
let schema: Schema = emptySchema()
@@ -74,6 +75,7 @@
loadTemplate()
}
}
$dirtyStore = true
</script>
<ScriptBuilder {script} />
@@ -13,6 +13,7 @@
import { workspaceStore } from '$lib/stores'
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
import { decodeState } from '$lib/utils'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
const initialState = $page.url.searchParams.get('state')
let scriptLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
@@ -30,6 +31,7 @@
hash: $page.params.hash
})
initialPath = script!.path
$dirtyStore = false
}
$: {
@@ -37,6 +39,7 @@
loadScript()
}
}
</script>
{#if script}