mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 16:02:28 +00:00
fix(frontend): Handle empty error message in toast + hide GridEditorM… (#3664)
* fix(frontend): Handle empty error message in toast + hide GridEditorMenu when the component is not visible * fix(frontend): Handle empty error message in toast + hide GridEditorMenu when the component is not visible * fix(frontend): use the right id
This commit is contained in:
@@ -1,118 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
interface ContextMenuRegistry {
|
||||
id: string
|
||||
close: () => void
|
||||
}
|
||||
|
||||
// Using a Svelte store for global state management
|
||||
const openedContextMenus = writable<Set<ContextMenuRegistry>>(new Set())
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import Portal from 'svelte-portal'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let id: string
|
||||
|
||||
let contextMenuVisible = false
|
||||
let menuX = 0
|
||||
let menuY = 0
|
||||
|
||||
interface MenuItem {
|
||||
label: string
|
||||
onClick: () => void
|
||||
color?: string
|
||||
icon: any
|
||||
shortcut?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface ContextMenu {
|
||||
menuItems: MenuItem[]
|
||||
}
|
||||
|
||||
export let contextMenu: ContextMenu = { menuItems: [] }
|
||||
|
||||
function handleRightClick(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
contextMenuVisible = true
|
||||
menuX = event.clientX
|
||||
menuY = event.clientY
|
||||
|
||||
openedContextMenus.update((menus) => {
|
||||
menus.forEach((menu) => menu.id !== id && menu.close())
|
||||
menus.clear()
|
||||
menus.add({ id, close: () => (contextMenuVisible = false) })
|
||||
return menus
|
||||
})
|
||||
}
|
||||
|
||||
function closeContextMenu() {
|
||||
contextMenuVisible = false
|
||||
openedContextMenus.update((menus) => {
|
||||
menus.clear()
|
||||
return menus
|
||||
})
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (contextMenuVisible) {
|
||||
closeContextMenu()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={handleClickOutside} />
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div on:click={closeContextMenu} on:contextmenu={handleRightClick} class="h-full w-full">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<slot />
|
||||
|
||||
{#if contextMenuVisible}
|
||||
<Portal>
|
||||
<div style="position: fixed; top: {menuY}px; left: {menuX}px; z-index:6000;">
|
||||
<div class="rounded-md bg-surface border shadow-md divide-y w-64">
|
||||
<div class="p-1" use:clickOutside={false}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#each contextMenu.menuItems as item}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<button
|
||||
class={twMerge(
|
||||
'flex items-center p-2 hover:bg-surface-hover cursor-pointer transition-all rounded-md w-full',
|
||||
item.color === 'red' && 'text-red-500',
|
||||
item.color === 'green' && 'text-green-500',
|
||||
item.color === 'blue' && 'text-blue-500',
|
||||
item.disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
on:click={() => {
|
||||
item.onClick()
|
||||
closeContextMenu()
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<svelte:component this={item.icon} class="w-4 h-4" />
|
||||
|
||||
<span class="ml-2 text-xs">{item.label}</span>
|
||||
{#if item.shortcut}
|
||||
<span class="ml-auto text-xs text-gray-400">
|
||||
{item.shortcut}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -187,9 +187,11 @@
|
||||
const appendError = sideEffect?.configuration?.sendErrorToast?.appendError
|
||||
|
||||
if (!message) return
|
||||
|
||||
if (typeof message === 'function') {
|
||||
message = await message()
|
||||
}
|
||||
|
||||
sendUserToast(message, true, [], appendError ? errorMessage : undefined)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,70 +1,156 @@
|
||||
<script context="module" lang="ts">
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
interface ContextMenuRegistry {
|
||||
id: string
|
||||
close: () => void
|
||||
}
|
||||
|
||||
// Using a Svelte store for global state management
|
||||
const openedContextMenus = writable<Set<ContextMenuRegistry>>(new Set())
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getModifierKey } from '$lib/utils'
|
||||
|
||||
import { Copy, Paintbrush2, Scissors, Trash } from 'lucide-svelte'
|
||||
import ContextMenu from '$lib/components/ContextMenu.svelte'
|
||||
import ComponentCallbacks from './component/ComponentCallbacks.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import DeleteComponent from './settingsPanel/DeleteComponent.svelte'
|
||||
import { findComponentSettings } from './appUtils'
|
||||
import { secondaryMenuLeft } from './settingsPanel/secondaryMenu'
|
||||
import StylePanel from './settingsPanel/StylePanel.svelte'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import Portal from 'svelte-portal'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
let contextMenuVisible = false
|
||||
let menuX = 0
|
||||
let menuY = 0
|
||||
|
||||
function handleRightClick(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
contextMenuVisible = true
|
||||
menuX = event.clientX
|
||||
menuY = event.clientY
|
||||
|
||||
openedContextMenus.update((menus) => {
|
||||
menus.forEach((menu) => menu.id !== id && menu.close())
|
||||
menus.clear()
|
||||
menus.add({ id, close: () => (contextMenuVisible = false) })
|
||||
return menus
|
||||
})
|
||||
}
|
||||
|
||||
function closeContextMenu() {
|
||||
contextMenuVisible = false
|
||||
openedContextMenus.update((menus) => {
|
||||
menus.clear()
|
||||
return menus
|
||||
})
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (contextMenuVisible) {
|
||||
closeContextMenu()
|
||||
}
|
||||
}
|
||||
|
||||
export let id: string
|
||||
let componentCallbacks: ComponentCallbacks | undefined = undefined
|
||||
|
||||
const { selectedComponent, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { movingcomponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let deleteComponent: DeleteComponent | undefined = undefined
|
||||
$: componentSettings = $selectedComponent?.map((sc) => findComponentSettings($app, sc))
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: 'Cut',
|
||||
onClick: () => {
|
||||
componentCallbacks?.handleCut(new KeyboardEvent('keydown'))
|
||||
},
|
||||
icon: Scissors,
|
||||
shortcut: `${getModifierKey()}X`,
|
||||
disabled: $movingcomponents?.includes($selectedComponent?.[0] ?? '')
|
||||
},
|
||||
{
|
||||
label: 'Copy',
|
||||
onClick: () => {
|
||||
componentCallbacks?.handleCopy(new KeyboardEvent('keydown'))
|
||||
},
|
||||
icon: Copy,
|
||||
shortcut: `${getModifierKey()}C`
|
||||
},
|
||||
{
|
||||
label: 'Show style panel',
|
||||
onClick: () => {
|
||||
secondaryMenuLeft?.toggle(StylePanel, { type: 'style' })
|
||||
},
|
||||
icon: Paintbrush2,
|
||||
disabled: $secondaryMenuLeft.isOpen
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: () => {
|
||||
deleteComponent?.removeGridElement()
|
||||
},
|
||||
icon: Trash,
|
||||
shortcut: `Del`,
|
||||
color: 'red'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<ComponentCallbacks bind:this={componentCallbacks} />
|
||||
<ContextMenu
|
||||
contextMenu={{
|
||||
menuItems: [
|
||||
{
|
||||
label: 'Cut',
|
||||
onClick: () => {
|
||||
componentCallbacks?.handleCut(new KeyboardEvent('keydown'))
|
||||
},
|
||||
icon: Scissors,
|
||||
shortcut: `${getModifierKey()}X`,
|
||||
disabled: $movingcomponents?.includes($selectedComponent?.[0] ?? '')
|
||||
},
|
||||
{
|
||||
label: 'Copy',
|
||||
onClick: () => {
|
||||
componentCallbacks?.handleCopy(new KeyboardEvent('keydown'))
|
||||
},
|
||||
icon: Copy,
|
||||
shortcut: `${getModifierKey()}C`
|
||||
},
|
||||
{
|
||||
label: 'Show style panel',
|
||||
onClick: () => {
|
||||
secondaryMenuLeft?.toggle(StylePanel, { type: 'style' })
|
||||
},
|
||||
icon: Paintbrush2,
|
||||
disabled: $secondaryMenuLeft.isOpen
|
||||
},
|
||||
<DeleteComponent bind:this={deleteComponent} />
|
||||
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: () => {
|
||||
deleteComponent?.removeGridElement()
|
||||
},
|
||||
icon: Trash,
|
||||
shortcut: `Del`,
|
||||
color: 'red'
|
||||
}
|
||||
]
|
||||
}}
|
||||
{id}
|
||||
>
|
||||
<svelte:window on:click={handleClickOutside} />
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div on:click={closeContextMenu} on:contextmenu={handleRightClick} class="h-full w-full">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<slot />
|
||||
</ContextMenu>
|
||||
|
||||
<DeleteComponent {componentSettings} bind:this={deleteComponent} />
|
||||
{#if contextMenuVisible}
|
||||
<Portal>
|
||||
<div style="position: fixed; top: {menuY}px; left: {menuX}px; z-index:6000;">
|
||||
<div class="rounded-md bg-surface border shadow-md divide-y w-64">
|
||||
<div class="p-1" use:clickOutside={false}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#each menuItems as item}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<button
|
||||
class={twMerge(
|
||||
'flex items-center p-2 hover:bg-surface-hover cursor-pointer transition-all rounded-md w-full',
|
||||
item.color === 'red' && 'text-red-500',
|
||||
item.color === 'green' && 'text-green-500',
|
||||
item.color === 'blue' && 'text-blue-500',
|
||||
item.disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
on:click={() => {
|
||||
item.onClick()
|
||||
closeContextMenu()
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<svelte:component this={item.icon} class="w-4 h-4" />
|
||||
|
||||
<span class="ml-2 text-xs">{item.label}</span>
|
||||
{#if item.shortcut}
|
||||
<span class="ml-auto text-xs text-gray-400">
|
||||
{item.shortcut}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -469,7 +469,7 @@ const onSuccessClick = {
|
||||
},
|
||||
sendToast: {
|
||||
message: {
|
||||
tooltip: 'The message of the toast to diplay',
|
||||
tooltip: 'The message of the toast to display',
|
||||
fieldType: 'text',
|
||||
type: 'static',
|
||||
value: '',
|
||||
@@ -557,10 +557,10 @@ const onErrorClick = {
|
||||
},
|
||||
sendErrorToast: {
|
||||
message: {
|
||||
tooltip: 'The message of the toast to diplay',
|
||||
tooltip: 'The message of the toast to display',
|
||||
fieldType: 'text',
|
||||
type: 'static',
|
||||
value: '',
|
||||
value: 'An error occured',
|
||||
placeholder: 'Hello there',
|
||||
onDemandOnly: true
|
||||
},
|
||||
@@ -3150,14 +3150,12 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
fieldType: 'boolean',
|
||||
type: 'static',
|
||||
value: false,
|
||||
|
||||
tooltip: 'This will diplay the type and/or the format on the field next to the label.'
|
||||
tooltip: 'This will display the type and/or the format on the field next to the label.'
|
||||
},
|
||||
largeGap: {
|
||||
fieldType: 'boolean',
|
||||
type: 'static',
|
||||
value: false,
|
||||
|
||||
tooltip: 'This will add a large gap between the form elements.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext, GridItem } from '../../types'
|
||||
import { deleteGridItem } from '../appUtils'
|
||||
import type { AppEditorContext, AppViewerContext } from '../../types'
|
||||
import { deleteGridItem, findComponentSettings } from '../appUtils'
|
||||
import { push } from '$lib/history'
|
||||
|
||||
export let componentSettings:
|
||||
| {
|
||||
item: GridItem
|
||||
parent: string
|
||||
}[]
|
||||
| any = []
|
||||
export let onDelete: (() => void) | undefined = undefined
|
||||
export let noGrid = false
|
||||
|
||||
@@ -26,45 +20,42 @@
|
||||
const { history, movingcomponents } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
export function removeGridElement() {
|
||||
const id = $selectedComponent?.[0]
|
||||
const componentSetting = findComponentSettings($app, id)
|
||||
push(history, $app)
|
||||
|
||||
componentSettings?.forEach((componentSetting) => {
|
||||
const id = componentSetting?.item?.id
|
||||
const onDeleteComponentControl = id ? $componentControl[id]?.onDelete : undefined
|
||||
if (onDeleteComponentControl) {
|
||||
onDeleteComponentControl()
|
||||
}
|
||||
if (onDelete) {
|
||||
onDelete()
|
||||
}
|
||||
|
||||
const onDeleteComponentControl = id ? $componentControl[id]?.onDelete : undefined
|
||||
if (onDeleteComponentControl) {
|
||||
onDeleteComponentControl()
|
||||
if (id) {
|
||||
delete $worldStore.outputsById[id]
|
||||
delete $errorByComponent[id]
|
||||
|
||||
if ($movingcomponents?.includes(id)) {
|
||||
$movingcomponents = $movingcomponents.filter((_id) => _id !== id)
|
||||
}
|
||||
if (onDelete) {
|
||||
onDelete()
|
||||
}
|
||||
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
if (componentSetting?.item && !noGrid) {
|
||||
let ids = deleteGridItem($app, componentSetting?.item.data, componentSetting?.parent)
|
||||
for (const key of ids) {
|
||||
delete $runnableComponents[key]
|
||||
}
|
||||
}
|
||||
|
||||
let cId = componentSetting?.item.id
|
||||
if (cId) {
|
||||
delete $worldStore.outputsById[cId]
|
||||
delete $errorByComponent[cId]
|
||||
if (componentSetting?.item?.data?.id) {
|
||||
delete $runnableComponents[componentSetting?.item?.data?.id]
|
||||
}
|
||||
$app = $app
|
||||
$runnableComponents = $runnableComponents
|
||||
|
||||
if ($movingcomponents?.includes(cId)) {
|
||||
$movingcomponents = $movingcomponents.filter((id) => id !== cId)
|
||||
}
|
||||
}
|
||||
|
||||
$selectedComponent = undefined
|
||||
$focusedGrid = undefined
|
||||
if (componentSetting?.item && !noGrid) {
|
||||
let ids = deleteGridItem($app, componentSetting?.item.data, componentSetting?.parent)
|
||||
for (const key of ids) {
|
||||
delete $runnableComponents[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (componentSetting?.item?.data?.id) {
|
||||
delete $runnableComponents[componentSetting?.item?.data?.id]
|
||||
}
|
||||
$app = $app
|
||||
$runnableComponents = $runnableComponents
|
||||
|
||||
onDelete?.()
|
||||
})
|
||||
onDelete?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
}
|
||||
|
||||
.multi-item {
|
||||
@apply bg-gray-100 mt-1 mr-1 border border-gray-200 rounded-sm h-8 leading-8 flex cursor-default pr-1 pl-1 max-w-full items-center mr-1 overflow-hidden overflow-ellipsis whitespace-nowrap;
|
||||
@apply bg-gray-100 mt-1 border border-gray-200 rounded-sm h-8 leading-8 flex cursor-default pr-1 pl-1 max-w-full items-center mr-1 overflow-hidden overflow-ellipsis whitespace-nowrap;
|
||||
}
|
||||
|
||||
.multi-item.disabled {
|
||||
|
||||
Reference in New Issue
Block a user