mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-09 00:02:30 +00:00
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:
@@ -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)
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user