feat(frontend): Deeply nested Modals and Drawers (#3565)

* fix(frontend): Support deeply nested drawers and modals

* fix(frontend): Support deeply nested drawers and modals

* fix(frontend): Support deeply nested drawers and modals

* feat(frontend): fix name

* chore(main): release 1.308.2 (#3569)

* chore(main): release 1.308.2

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>

* feat(frontend): wip

* feat(frontend): wip

* fix(frontend): fix event propagation

* fix(frontend): fix drawers globally

* feat(frontend): Fix all drawers

* feat(frontend): split zIndexes in a new file

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
This commit is contained in:
Faton Ramadani
2024-04-19 18:58:39 +02:00
committed by GitHub
parent 5a40df90aa
commit 8cb7d29cf8
15 changed files with 570 additions and 639 deletions
+278 -410
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -5554,7 +5554,6 @@ paths:
text/plain:
type: string
/w/{workspace}/jobs_u/get_flow_debug_info/{id}:
get:
summary: get flow debug info
@@ -8764,14 +8763,14 @@ components:
Job:
oneOf:
- allOf:
- allOf:
- $ref: "#/components/schemas/CompletedJob"
- type: object
properties:
type:
type: string
enum: [CompletedJob]
- allOf:
- allOf:
- $ref: "#/components/schemas/QueuedJob"
- type: object
properties:
@@ -9941,7 +9940,7 @@ components:
created_at:
type: string
format: date-time
value:
value:
type: object
policy:
$ref: "#/components/schemas/Policy"
@@ -5,19 +5,16 @@
import Drawer from './common/drawer/Drawer.svelte'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import DefaultScriptsInner from './DefaultScriptsInner.svelte'
import Portal from 'svelte-portal'
let drawer: Drawer
</script>
{#if $userStore?.is_admin || $userStore?.is_super_admin}
<Portal>
<Drawer bind:this={drawer} placement="left">
<DrawerContent title="Edit Default Scripts" on:close={drawer.closeDrawer}>
<DefaultScriptsInner />
</DrawerContent>
</Drawer>
</Portal>
<Drawer bind:this={drawer} placement="left">
<DrawerContent title="Edit Default Scripts" on:close={drawer.closeDrawer}>
<DefaultScriptsInner />
</DrawerContent>
</Drawer>
<Button
on:click={drawer?.openDrawer}
startIcon={{ icon: SettingsIcon }}
@@ -588,46 +588,44 @@
</div>
{#if !disableExpand && !noControls}
<Portal>
<Drawer bind:this={jsonViewer} size="900px">
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
<Button
download="{filename ?? 'result'}.json"
href={workspaceId && jobId
? `/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
startIcon={{ icon: Download }}
color="light"
size="xs"
>
Download
</Button>
<Button
on:click={() => copyToClipboard(toJsonStr(result))}
color="light"
size="xs"
startIcon={{
icon: ClipboardCopy
}}
>
Copy to clipboard
</Button>
</svelte:fragment>
<svelte:self
{noControls}
{result}
{requireHtmlApproval}
{filename}
{jobId}
{workspaceId}
{hideAsJson}
{forceJson}
disableExpand={true}
/>
</DrawerContent>
</Drawer>
</Portal>
<Drawer bind:this={jsonViewer} size="900px">
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
<svelte:fragment slot="actions">
<Button
download="{filename ?? 'result'}.json"
href={workspaceId && jobId
? `/api/w/${workspaceId}/jobs_u/completed/get_result/${jobId}`
: `data:text/json;charset=utf-8,${encodeURIComponent(toJsonStr(result))}`}
startIcon={{ icon: Download }}
color="light"
size="xs"
>
Download
</Button>
<Button
on:click={() => copyToClipboard(toJsonStr(result))}
color="light"
size="xs"
startIcon={{
icon: ClipboardCopy
}}
>
Copy to clipboard
</Button>
</svelte:fragment>
<svelte:self
{noControls}
{result}
{requireHtmlApproval}
{filename}
{jobId}
{workspaceId}
{hideAsJson}
{forceJson}
disableExpand={true}
/>
</DrawerContent>
</Drawer>
<Portal>
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
@@ -114,6 +114,7 @@
size="800px"
alwaysOpen
positionClass={$mode == 'dnd' ? '!absolute' : '!fixed'}
shouldUsePortal={false}
>
<DrawerContent
title={resolvedConfig.drawerTitle}
@@ -14,6 +14,7 @@
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import Disposable from '$lib/components/common/drawer/Disposable.svelte'
export let customCss: ComponentCustomCSS<'modalcomponent'> | undefined = undefined
export let id: string
@@ -37,22 +38,7 @@
initOutput($worldStore, id, {})
let css = initCss($app.css?.modalcomponent, customCss)
let open = false
function handleKeyUp(event: KeyboardEvent): void {
const key = event.key
if (key === 'Escape' || key === 'Esc') {
if (open) {
event.preventDefault()
closeDrawer()
}
}
}
function closeDrawer(): void {
console.log('Close drawer')
open = false
}
let disposable: Disposable | undefined = undefined
let resolvedConfig = initConfig(
components['modalcomponent'].initialData.configuration,
@@ -69,23 +55,22 @@
$componentControl[id] = {
openModal: () => {
unclosableModal()
open = true
disposable?.openDrawer()
},
closeModal: () => {
open = false
disposable?.closeDrawer()
},
open: () => {
unclosableModal()
open = true
disposable?.openDrawer()
},
close: () => {
open = false
disposable?.closeDrawer()
}
}
</script>
<svelte:window on:keyup={handleKeyUp} />
<InitializeComponent {id} />
{#each Object.keys(components['modalcomponent'].initialData.configuration) as key (key)}
@@ -132,7 +117,7 @@
parentComponentId: id,
subGridIndex: 0
}
open = true
disposable?.toggleDrawer()
}}
size={resolvedConfig.buttonSize}
color={resolvedConfig.buttonColor}
@@ -144,69 +129,71 @@
{/if}
<Portal target="#app-editor-top-level-drawer">
<div
class={twMerge(
`${
$mode == 'dnd' ? 'absolute' : 'fixed'
} top-0 bottom-0 left-0 right-0 transition-all duration-50`,
open ? ' bg-black bg-opacity-60' : 'h-0 overflow-hidden invisible',
$mode === 'dnd' ? 'z-[1000]' : 'z-[1100]'
)}
>
<Disposable {id} let:handleClickAway let:zIndex let:open bind:this={disposable}>
<div
style={css?.popup?.style}
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
use:clickOutside={false}
on:click_outside={(e) => {
if ($mode !== 'dnd' && !unclickableOutside) {
closeDrawer()
}
}}
class={twMerge(
`${
$mode == 'dnd' ? 'absolute' : 'fixed'
} top-0 bottom-0 left-0 right-0 transition-all duration-50`,
open ? ' bg-black bg-opacity-60' : 'h-0 overflow-hidden invisible'
)}
style="z-index: {zIndex}"
>
<div class="px-4 py-2 border-b flex justify-between items-center">
<div>{resolvedConfig.modalTitle}</div>
<div class="w-8">
<button
on:click|stopPropagation={() => {
open = false
}}
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<X class="text-tertiary" />
</button>
</div>
</div>
<div
class="wm-modal"
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
style={css?.popup?.style}
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
use:clickOutside={false}
on:click_outside={(e) => {
if ($mode !== 'dnd' && !unclickableOutside) {
handleClickAway(e)
}
}}
>
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{id}
noPadding
subGridId={`${id}-0`}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
<div class="px-4 py-2 border-b flex justify-between items-center">
<div>{resolvedConfig.modalTitle}</div>
<div class="w-8">
<button
on:click|stopPropagation={() => {
disposable?.closeDrawer()
}}
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<X class="text-tertiary" />
</button>
</div>
</div>
<div
class="wm-modal"
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}}
/>
{/if}
}
}}
>
{#if $app.subgrids?.[`${id}-0`]}
<SubGridEditor
visible={open && render}
{id}
noPadding
subGridId={`${id}-0`}
on:focus={() => {
if (!$connectingInput.opened) {
$selectedComponent = [id]
$focusedGrid = {
parentComponentId: id,
subGridIndex: 0
}
}
}}
/>
{/if}
</div>
</div>
</div>
</div>
</Disposable>
</Portal>
@@ -963,7 +963,7 @@
</DrawerContent>
</Drawer>
<Drawer bind:open={$jobsDrawerOpen} size="900px" --zIndex={1003}>
<Drawer bind:open={$jobsDrawerOpen} size="900px">
<DrawerContent
noPadding
title="Debug Runs"
@@ -10,6 +10,7 @@
import Component from './component/Component.svelte'
import ComponentWrapper from './component/ComponentWrapper.svelte'
import GridViewer from './GridViewer.svelte'
import GridEditorMenu from './GridEditorMenu.svelte'
export let containerHeight: number | undefined = undefined
export let containerWidth: number | undefined = undefined
@@ -120,31 +121,33 @@
'top-0'
)}
>
<Component
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
on:lock={() => lock(dataItem)}
on:expand={() => {
const parentGridItem = findGridItem($app, id)
<GridEditorMenu id={dataItem.id}>
<Component
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
on:lock={() => lock(dataItem)}
on:expand={() => {
const parentGridItem = findGridItem($app, id)
if (!parentGridItem) {
return
}
if (!parentGridItem) {
return
}
$selectedComponent = [dataItem.id]
push(editorContext?.history, $app)
$selectedComponent = [dataItem.id]
push(editorContext?.history, $app)
expandGriditem(
$app.subgrids?.[subGridId] ?? [],
dataItem.id,
$breakpoint,
parentGridItem
)
$app = $app
}}
/>
expandGriditem(
$app.subgrids?.[subGridId] ?? [],
dataItem.id,
$breakpoint,
parentGridItem
)
$app = $app
}}
/>
</GridEditorMenu>
</ComponentWrapper>
</Grid>
</div>
@@ -4,6 +4,7 @@
import { ClearableInput } from '../../../../common'
import { createPopperActions } from 'svelte-popperjs'
import { fade } from 'svelte/transition'
import { zIndexes } from '$lib/zIndexes'
export let value: string = '#fff'
const dispatch = createEventDispatcher()
@@ -25,7 +26,7 @@
<div
transition:fade={{ duration: 150 }}
use:popperContent={{ placement: 'bottom', strategy: 'fixed' }}
class="color-picker-input z-[1002] !text-primary"
class={`color-picker-input z-[${zIndexes.colorInput}] !text-primary`}
style="width: {width > 280 ? width : 280}px;"
>
<ColorPicker
@@ -4,6 +4,7 @@
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../../types'
import CloseButton from '$lib/components/common/CloseButton.svelte'
import { zIndexes } from '$lib/zIndexes'
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
export let right: boolean
@@ -18,10 +19,9 @@
}
</script>
<!-- z-index must be above the split pane handles' z-index (which is 1001 atm.) -->
<div
bind:clientWidth={width}
class="absolute z-[1002] inset-0 overflow-hidden w-full"
class={`absolute z-[${zIndexes.secondaryMenu}] inset-0 overflow-hidden w-full`}
class:pointer-events-none={!$secondaryMenu.isOpen}
>
{#if $secondaryMenu.isOpen && $secondaryMenu.component}
@@ -0,0 +1,11 @@
<script lang="ts">
import Portal from 'svelte-portal'
export let condition = false
</script>
{#if condition}
<Portal><slot /></Portal>
{:else}
<slot />
{/if}
@@ -0,0 +1,83 @@
<script lang="ts" context="module">
export let openedDrawers: string[] = []
</script>
<script lang="ts">
import { zIndexes } from '$lib/zIndexes'
import { createEventDispatcher } from 'svelte'
export let open = false
export let id = (Math.random() + 1).toString(36).substring(10)
if (open) {
openedDrawers.push(id)
}
let offset = 0
export function toggleDrawer() {
if (!open) {
openDrawer()
} else {
closeDrawer()
}
}
export function openDrawer() {
open = true
offset = openedDrawers.length - 1
if (openedDrawers.includes(id)) {
return
}
openedDrawers.push(id)
}
export function closeDrawer() {
open = false
offset = 0
// remove the last opened drawer
openedDrawers = openedDrawers.filter((drawer) => drawer !== id)
}
export function isOpen() {
return open
}
const dispatch = createEventDispatcher()
function handleClickAway(e) {
const last = openedDrawers[openedDrawers.length - 1]
if (last === id) {
e.stopPropagation()
closeDrawer()
}
}
function onKeyDown(event: KeyboardEvent) {
if (open) {
switch (event.key) {
case 'Escape':
if (id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) {
openedDrawers.pop()
event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
closeDrawer()
break
}
}
}
}
$: zIndex = zIndexes.disposables + offset
$: open ? dispatch('open') : dispatch('close')
</script>
<svelte:window on:keydown={onKeyDown} />
<slot {handleClickAway} {zIndex} {closeDrawer} {open} />
@@ -1,38 +1,30 @@
<script lang="ts" context="module">
let openedDrawers: string[] = []
</script>
<script lang="ts">
import { onMount } from 'svelte'
import { createEventDispatcher } from 'svelte'
import { onMount, createEventDispatcher } from 'svelte'
import { BROWSER } from 'esm-env'
import Disposable from './Disposable.svelte'
import ConditionalPortal from './ConditionalPortal.svelte'
export let open = false
export let duration = 0.3
export let placement = 'right'
export let size = '600px'
export let alwaysOpen = false
export let shouldUsePortal: boolean = true
let id = (Math.random() + 1).toString(36).substring(10)
let disposable: Disposable | undefined = undefined
$: durationMs = duration * 1000
export function toggleDrawer() {
open = !open
if (open) {
openedDrawers.push(id)
} else {
openedDrawers = openedDrawers.filter((x) => x != id)
}
disposable?.toggleDrawer()
}
export function openDrawer() {
openedDrawers.push(id)
open = true
disposable?.openDrawer()
}
export function closeDrawer() {
open = false
openedDrawers = openedDrawers.filter((x) => x != id)
disposable?.closeDrawer()
setTimeout(() => {
dispatch('afterClose')
@@ -60,28 +52,7 @@
$: scrollLock(open)
function handleClickAway() {
dispatch('clickAway')
open = false
}
function onKeyDown(event: KeyboardEvent) {
if (open) {
switch (event.key) {
case 'Escape':
if (id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) {
openedDrawers.pop()
event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
open = false
break
}
}
}
}
$: open ? dispatch('open') : dispatch('close')
$: open ? openDrawer() : closeDrawer()
let timeout = true
$: !open ? setTimeout(() => (timeout = true), durationMs) : (timeout = false)
@@ -90,22 +61,25 @@
})
</script>
<svelte:window on:keydown={onKeyDown} />
<aside
class="drawer {$$props.class ?? ''} {$$props.positionClass ?? ''}"
class:open
class:close={!open && timeout}
{style}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="overlay {$$props.positionClass ?? ''}" on:click={handleClickAway} />
<div class="panel {placement} {$$props.positionClass}" class:size>
{#if open || !timeout || alwaysOpen}
<slot {open} />
{/if}
</div>
</aside>
<ConditionalPortal condition={shouldUsePortal}>
<Disposable let:handleClickAway let:zIndex bind:open bind:this={disposable} on:open on:close>
<aside
class="drawer {$$props.class ?? ''} {$$props.positionClass ?? ''}"
class:open
class:close={!open && timeout}
style={`${style}; --zIndex: ${zIndex};`}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="overlay {$$props.positionClass ?? ''}" on:click={handleClickAway} />
<div class="panel {placement} {$$props.positionClass}" class:size>
{#if open || !timeout || alwaysOpen}
<slot {open} />
{/if}
</div>
</aside>
</Disposable>
</ConditionalPortal>
<style>
.drawer {
@@ -123,7 +97,7 @@
.drawer.open {
height: 100%;
width: 100%;
z-index: var(--zIndex, 1002);
z-index: var(--zIndex);
transition: z-index var(--duration) step-start;
pointer-events: auto;
}
+12
View File
@@ -0,0 +1,12 @@
export const zIndexes = {
appEditorComponents: 1000,
secondaryMenu: 1001,
splitPanelHandle: 1001,
colorInput: 1002,
disposables: 1100, // Modals and Drawers
svelteSelectOptions: 5000,
contextMenu: 6000,
draggingComponent: 10000,
monacoEditor: 10000,
monacoEditorSuggestions: 10001
}
@@ -50,7 +50,6 @@
} from 'lucide-svelte'
import { onMount } from 'svelte'
import autosize from '$lib/autosize'
import Portal from 'svelte-portal'
type ResourceW = ListableResource & { canWrite: boolean; marked?: string }
type ResourceTypeW = ResourceType & { canWrite: boolean }
@@ -360,21 +359,19 @@
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
<Portal>
<Drawer bind:this={inferrer} size="800px">
<DrawerContent title="Infer type from JSON" on:close={() => inferrer?.toggleDrawer?.()}>
<SimpleEditor
bind:code={inferrerJson}
lang="json"
class="h-full"
fixedOverflowWidgets={false}
/>
<svelte:fragment slot="actions">
<Button size="sm" on:click={inferJson}>Infer</Button>
</svelte:fragment>
</DrawerContent>
</Drawer>
</Portal>
<Drawer bind:this={inferrer} size="800px">
<DrawerContent title="Infer type from JSON" on:close={() => inferrer?.toggleDrawer?.()}>
<SimpleEditor
bind:code={inferrerJson}
lang="json"
class="h-full"
fixedOverflowWidgets={false}
/>
<svelte:fragment slot="actions">
<Button size="sm" on:click={inferJson}>Infer</Button>
</svelte:fragment>
</DrawerContent>
</Drawer>
<Drawer bind:this={resourceTypeViewer} size="800px">
<DrawerContent title={resourceTypeViewerObj.rt} on:close={resourceTypeViewer.closeDrawer}>