fix(frontend): prevent deploy popover to show if deploy dropdown is open (#5542)

* prevent deploy popover to show if deploy dropdown is open

* wip

* Revert "wip"

This reverts commit 85434654af.

* Revert "prevent deploy popover to show if deploy dropdown is open"

This reverts commit edd9eda156.

* add prop to hide popup fro dropdown

* feat: button can have tooltip

* improve deploy tooltip and dropdown behavior

* rename tooltip to tooltipPopover

* nit

* add deploy button component

* use svelte 5 runes

* use new deploy button for script builder

* add delay to deploy popover when dropdown is open

* add delay to deploy popover when dropdown is open

# Conflicts:
#	frontend/src/lib/components/DeployButton.svelte
#	frontend/src/lib/components/common/button/Button.svelte

* Update frontend/src/lib/components/common/button/Button.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* remove unsused field

* nit

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Guilhem
2025-04-07 13:43:00 +01:00
committed by GitHub
parent 1007f37f4b
commit c2180c6eb3
5 changed files with 152 additions and 95 deletions
@@ -0,0 +1,80 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { Save, CornerDownLeft } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
const {
loading = false,
loadingSave = false,
newFlow = false,
dropdownItems = []
} = $props<{
loading?: boolean
loadingSave?: boolean
newFlow?: boolean
dropdownItems?: Array<{
label: string
onClick: () => void
}>
}>()
const dispatch = createEventDispatcher()
let msgInput: HTMLInputElement | undefined = $state(undefined)
let hideDropdown = $state(false)
let deploymentMsg = $state('')
let dropdownOpen = $state(false)
</script>
<Button
disabled={loading}
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={() => dispatch('save')}
dropdownItems={!newFlow ? dropdownItems : undefined}
tooltipPopover={{
placement: 'bottom-end',
openDelay: dropdownOpen ? 200 : 0,
closeDelay: 0,
portal: 'body'
}}
on:tooltipOpen={async ({ detail }) => {
if (detail) {
// Use setTimeout to ensure DOM is updated
setTimeout(() => {
msgInput?.focus()
}, 0)
hideDropdown = true
} else {
hideDropdown = false
}
}}
{hideDropdown}
on:dropdownOpen={({ detail }) => (dropdownOpen = detail)}
>
Deploy
<svelte:fragment slot="tooltip">
<div class="flex flex-row gap-2 w-80 p-4 bg-surface rounded-lg shadow-lg border z-[5001]">
<input
type="text"
placeholder="Deployment message"
bind:value={deploymentMsg}
onkeydown={async (e) => {
if (e.key === 'Enter') {
dispatch('save', deploymentMsg)
}
}}
bind:this={msgInput}
/>
<Button
size="xs"
on:click={async () => dispatch('save', deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>
Deploy
</Button>
</div>
</svelte:fragment>
</Button>
@@ -23,6 +23,8 @@
export let usePointerDownOutside = false
export let closeOnOtherDropdownOpen = true
export let fixedHeight = true
export let hidePopup = false
export let open = false
const {
elements: { menu, item, trigger },
@@ -50,7 +52,6 @@
}
})
let open = false
const sync = createSync(states)
$: sync.open(open, (v) => (open = Boolean(v)))
@@ -104,7 +105,7 @@
{/if}
</button>
{#if open}
{#if open && !hidePopup}
<div use:melt={$menu} data-menu class="z-[6000]">
<div
class="bg-surface border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto py-1 max-h-[50vh]"
+11 -52
View File
@@ -48,16 +48,7 @@
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
import { cleanInputs, emptyFlowModuleState } from './flows/utils'
import {
Calendar,
Pen,
Save,
DiffIcon,
HistoryIcon,
FileJson,
type Icon,
CornerDownLeft
} from 'lucide-svelte'
import { Calendar, Pen, Save, DiffIcon, HistoryIcon, FileJson, type Icon } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Awareness from './Awareness.svelte'
import { getAllModules } from './flows/flowExplorer'
@@ -79,12 +70,12 @@
import { ignoredTutorials } from './tutorials/ignoredTutorials'
import type DiffDrawer from './DiffDrawer.svelte'
import FlowHistory from './flows/FlowHistory.svelte'
import CustomPopover from './CustomPopover.svelte'
import Summary from './Summary.svelte'
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
import { type TriggerContext, type ScheduleTrigger } from './triggers'
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
import DeployButton from './DeployButton.svelte'
export let initialPath: string = ''
export let pathStoreInit: string | undefined = undefined
@@ -1256,9 +1247,6 @@
]
}
let deploymentMsg = ''
let msgInput: HTMLInputElement | undefined = undefined
let flowPreviewButtons: FlowPreviewButtons
</script>
@@ -1303,7 +1291,8 @@
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 scrollbar-hidden overflow-x-auto max-h-12 h-full relative"
>
{#if $copilotCurrentStepStore !== undefined}
<div transition:fade class="absolute inset-0 bg-gray-500 bg-opacity-75 z-[900] !m-0"></div>
<div transition:fade class="absolute inset-0 bg-gray-500 bg-opacity-75 z-[900] !m-0"
></div>
{/if}
<div class="flex w-full max-w-md gap-4 items-center">
<Summary
@@ -1468,43 +1457,13 @@
Draft
</Button>
<CustomPopover appearTimeout={0} focusEl={msgInput}>
<Button
disabled={loading}
loading={loadingSave}
size="xs"
startIcon={{ icon: Save }}
on:click={async () => {
await handleSaveFlow()
}}
dropdownItems={!newFlow ? dropdownItems : undefined}
>
Deploy
</Button>
<svelte:fragment slot="overlay">
<div class="flex flex-row gap-2 w-80">
<input
type="text"
placeholder="Deployment message"
bind:value={deploymentMsg}
on:keydown={async (e) => {
if (e.key === 'Enter') {
await handleSaveFlow(deploymentMsg)
}
}}
bind:this={msgInput}
/>
<Button
size="xs"
on:click={async () => await handleSaveFlow(deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>
Deploy
</Button>
</div>
</svelte:fragment>
</CustomPopover>
<DeployButton
on:save={async ({ detail }) => await handleSaveFlow(detail)}
{loading}
{loadingSave}
{newFlow}
{dropdownItems}
/>
</div>
</div>
@@ -39,7 +39,6 @@
Calendar,
CheckCircle,
Code,
CornerDownLeft,
Pen,
Plus,
Rocket,
@@ -65,7 +64,6 @@
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import DefaultScripts from './DefaultScripts.svelte'
import { createEventDispatcher, onMount, setContext } from 'svelte'
import CustomPopover from './CustomPopover.svelte'
import Summary from './Summary.svelte'
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
import DeployOverrideConfirmationModal from '$lib/components/common/confirmationModal/DeployOverrideConfirmationModal.svelte'
@@ -80,6 +78,8 @@
import CaptureTable from './triggers/CaptureTable.svelte'
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
import DeployButton from './DeployButton.svelte'
export let script: NewScript
export let fullyLoaded: boolean = true
export let initialPath: string = ''
@@ -757,9 +757,6 @@
setContext('disableTooltips', customUi?.disableTooltips === true)
let deploymentMsg = ''
let msgInput: HTMLInputElement | undefined = undefined
function langToLanguage(lang: SupportedLanguage | 'docker' | 'bunnative'): SupportedLanguage {
if (lang == 'docker') {
return 'bash'
@@ -1575,41 +1572,13 @@
<span class="hidden lg:flex"> Draft </span>
</Button>
<CustomPopover appearTimeout={0} focusEl={msgInput}>
<Button
loading={loadingSave}
size="xs"
disabled={!fullyLoaded}
startIcon={{ icon: Save }}
on:click={() => handleEditScript(false)}
dropdownItems={computeDropdownItems(initialPath, savedScript, diffDrawer)}
>
Deploy
</Button>
<svelte:fragment slot="overlay">
<div class="flex flex-row gap-2 min-w-72">
<input
type="text"
placeholder="Deployment message"
bind:value={deploymentMsg}
bind:this={msgInput}
on:keydown={(e) => {
if (e.key === 'Enter') {
handleEditScript(false, deploymentMsg)
}
}}
/>
<Button
size="xs"
on:click={() => handleEditScript(false, deploymentMsg)}
endIcon={{ icon: CornerDownLeft }}
loading={loadingSave}
>
Deploy
</Button>
</div>
</svelte:fragment>
</CustomPopover>
<DeployButton
loading={!fullyLoaded}
{loadingSave}
newFlow={false}
dropdownItems={computeDropdownItems(initialPath, savedScript, diffDrawer)}
on:save={({ detail }) => handleEditScript(false, detail)}
/>
</div>
</div>
</div>
@@ -5,6 +5,9 @@
import Dropdown from '$lib/components/DropdownV2.svelte'
import { getModifierKey, type Item } from '$lib/utils'
import { Loader2, ChevronDown } from 'lucide-svelte'
import { createTooltip } from '@melt-ui/svelte'
import type { Placement } from '@floating-ui/core'
import { conditionalMelt } from '$lib/utils'
export let size: ButtonType.Size = 'md'
export let spacingSize: ButtonType.Size = size
@@ -34,6 +37,14 @@
export let shortCut:
| { key?: string; hide?: boolean; Icon?: any; withoutModifier?: boolean }
| undefined = undefined
export let tooltipPopover:
| {
placement?: Placement
openDelay?: number
closeDelay?: number
portal?: string
}
| undefined = undefined
type MenuItem = {
label: string
@@ -43,6 +54,7 @@
disabled?: boolean
}
export let dropdownItems: MenuItem[] | (() => MenuItem[]) | undefined = undefined
export let hideDropdown: boolean = false
function computeDropdowns(menuItems: MenuItem[] | (() => MenuItem[])): Item[] {
const items = typeof menuItems === 'function' ? menuItems() : menuItems
@@ -117,6 +129,28 @@
}
$: lucideIconSize = (iconMap[size] ?? 12) * 1
const {
elements: { trigger, content },
states: { open },
options: { openDelay }
} = tooltipPopover
? createTooltip({
positioning: {
placement: tooltipPopover?.placement
},
closeDelay: tooltipPopover?.closeDelay,
group: true,
portal: tooltipPopover?.portal
})
: {
elements: { trigger: undefined, content: undefined },
states: { open: undefined },
options: { openDelay: undefined }
}
$: tooltipPopover && openDelay !== undefined && ($openDelay = tooltipPopover?.openDelay) //This option is reactive
$: $open !== undefined && dispatch('tooltipOpen', $open)
</script>
<div
@@ -200,6 +234,8 @@
{...$$restProps}
disabled={disabled || (loading && !clickableWhileLoading)}
{style}
use:conditionalMelt={trigger}
{...$trigger}
>
{#if loading}
<Loader2 class={twMerge('animate-spin', iconOnlyPadding[size])} size={lucideIconSize} />
@@ -230,10 +266,22 @@
</div>
{/if}
</button>
{#if tooltipPopover && $open}
<div use:conditionalMelt={content} {...$content} class="z-[20000]">
<slot name="tooltip" />
</div>
{/if}
{/if}
{#if dropdownItems && dropdownItems.length > 0}
<Dropdown items={computeDropdowns(dropdownItems)} class="h-auto w-fit">
<Dropdown
items={computeDropdowns(dropdownItems)}
class="h-auto w-fit"
hidePopup={hideDropdown}
usePointerDownOutside
on:open={() => dispatch('dropdownOpen', true)}
on:close={() => dispatch('dropdownOpen', false)}
>
<svelte:fragment slot="buttonReplacement">
<div
class={twMerge(