Menu refactor (#1372)

* wip menu refactor

* wip

* wip

* feat(frontend): finish menu refactor

* feat(frontend): finish menu refactor
This commit is contained in:
Faton Ramadani
2023-04-07 00:08:53 +02:00
committed by GitHub
parent 266b5b00da
commit 368cdefd91
16 changed files with 189 additions and 263 deletions
+17
View File
@@ -37,6 +37,7 @@
},
"devDependencies": {
"@playwright/test": "^1.31.1",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.1",
"@sveltejs/kit": "^1.0.0-next.589",
"@sveltejs/package": "^1.0.2",
@@ -902,6 +903,15 @@
"resolved": "https://registry.npmjs.org/@redocly/json-to-json-schema/-/json-to-json-schema-0.0.1.tgz",
"integrity": "sha512-dSMyuNPN2k+tFeNZ0+QJ7S1zDJ0UeNL+lpnPFR9K5avj2V4uG4m6FdjrApQ9Zi35AIocaDp/KGfBD9gR5MLUbQ=="
},
"node_modules/@rgossiaux/svelte-headlessui": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rgossiaux/svelte-headlessui/-/svelte-headlessui-1.0.2.tgz",
"integrity": "sha512-sauopYTSivhzXe1kAvgawkhyYJcQlK8Li3p0d2OtcCIVprOzdbard5lbqWB4xHDv83zAobt2mR08oizO2poHLQ==",
"dev": true,
"peerDependencies": {
"svelte": "^3.44.0"
}
},
"node_modules/@sveltejs/adapter-static": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.1.tgz",
@@ -7846,6 +7856,13 @@
"resolved": "https://registry.npmjs.org/@redocly/json-to-json-schema/-/json-to-json-schema-0.0.1.tgz",
"integrity": "sha512-dSMyuNPN2k+tFeNZ0+QJ7S1zDJ0UeNL+lpnPFR9K5avj2V4uG4m6FdjrApQ9Zi35AIocaDp/KGfBD9gR5MLUbQ=="
},
"@rgossiaux/svelte-headlessui": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rgossiaux/svelte-headlessui/-/svelte-headlessui-1.0.2.tgz",
"integrity": "sha512-sauopYTSivhzXe1kAvgawkhyYJcQlK8Li3p0d2OtcCIVprOzdbard5lbqWB4xHDv83zAobt2mR08oizO2poHLQ==",
"dev": true,
"requires": {}
},
"@sveltejs/adapter-static": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.1.tgz",
+1
View File
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@playwright/test": "^1.31.1",
"@rgossiaux/svelte-headlessui": "^1.0.2",
"@sveltejs/adapter-static": "^2.0.1",
"@sveltejs/kit": "^1.0.0-next.589",
"@sveltejs/package": "^1.0.2",
+24 -14
View File
@@ -8,7 +8,7 @@
import { setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Badge, Button, ButtonPopup, ButtonPopupItem, UndoRedo } from './common'
import { Badge, Button, UndoRedo } from './common'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import FlowEditor from './flows/FlowEditor.svelte'
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
@@ -263,6 +263,23 @@
...dfs($flowStore.value.modules, (module) => module.id)
]
}
const dropdownItems: Array<{
label: string
onClick: () => void
}> = [
{
label: 'Save and exit',
onClick: () => saveFlow(true)
}
]
if (initialPath != '') {
dropdownItems.push({
label: 'Fork',
onClick: () => window.open(`/flows/add?template=${initialPath}`)
})
}
</script>
<svelte:window on:keydown={onKeyDown} />
@@ -273,7 +290,7 @@
<div class="flex flex-col flex-1 h-screen">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 overflow-x-auto scrollbar-hidden max-h-12 h-full"
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 scrollbar-hidden max-h-12 h-full"
>
<div class="flex w-full max-w-md gap-4 items-center">
<div class="min-w-64 w-full">
@@ -297,7 +314,7 @@
/>
</div>
<div class="gap-4 flex-row hidden md:flex w-full max-w-md overflow-hidden">
<div class="gap-4 flex-row hidden md:flex w-full max-w-md">
{#if $scheduleStore.enabled}
<Button
btnClasses="hidden lg:inline-flex"
@@ -344,22 +361,15 @@
<FlowPreviewButtons />
<div class="center-center">
<ButtonPopup
<Button
loading={loadingSave}
size="xs"
startIcon={{ icon: faSave }}
on:click={() => saveFlow(false)}
{dropdownItems}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => saveFlow(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/flows/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
Save
</Button>
</div>
</div>
</div>
@@ -10,7 +10,7 @@
import ScriptEditor from './ScriptEditor.svelte'
import ScriptSchema from './ScriptSchema.svelte'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import { Badge, Button, ButtonPopup, ButtonPopupItem, Drawer } from './common'
import { Badge, Button, Drawer } from './common'
import { faSave } from '@fortawesome/free-solid-svg-icons'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
@@ -125,6 +125,22 @@
}
loadingSave = false
}
const dropdownItems: Array<{ label: string; onClick: () => void }> = [
{
label: 'Save and leave',
onClick: () => editScript(true)
}
]
if (initialPath != '') {
dropdownItems.push({
label: 'Fork',
onClick: () => {
window.open(`/scripts/add?template=${initialPath}`)
}
})
}
</script>
{#if !$userStore?.operator}
@@ -269,23 +285,16 @@
>
Customise
</Button>
<ButtonPopup
<Button
color="dark"
loading={loadingSave}
size="sm"
startIcon={{ icon: faSave }}
on:click={() => editScript(false)}
{dropdownItems}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem on:click={() => editScript(true)}>Save and exit</ButtonPopupItem>
{#if initialPath != ''}
<ButtonPopupItem
on:click={() => {
window.open(`/scripts/add?template=${initialPath}`)
}}>Fork</ButtonPopupItem
>
{/if}
</ButtonPopup>
Save
</Button>
</div>
</div>
</div>
@@ -1,15 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import {
Alert,
Badge,
ButtonPopup,
ButtonPopupItem,
Drawer,
DrawerContent,
UndoRedo
} from '$lib/components/common'
import { Alert, Badge, Drawer, DrawerContent, UndoRedo } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
@@ -672,20 +664,23 @@
<span class="hidden md:inline">Save</span>
</Button>
{:else}
<ButtonPopup
<Button
loading={loading.save}
startIcon={{ icon: faSave }}
on:click={save}
color="dark"
size="xs"
dropdownItems={[
{
label: 'Fork',
onClick: () => {
window.open(`/apps/add?template=${appPath}`)
}
}
]}
>
<svelte:fragment slot="main">Save</svelte:fragment>
<ButtonPopupItem
on:click={() => {
window.open(`/apps/add?template=${appPath}`)
}}>Fork</ButtonPopupItem
>
</ButtonPopup>
Save
</Button>
{/if}
</div>
</div>
@@ -4,8 +4,9 @@
import { ButtonType } from './model'
import { goto } from '$app/navigation'
import { Loader2 } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import ButtonDropdown from './ButtonDropdown.svelte'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
export let size: ButtonType.Size = 'md'
export let spacingSize: ButtonType.Size = size
@@ -26,6 +27,13 @@
export let title: string | undefined = undefined
export let style: string = ''
type MenuItem = {
label: string
onClick?: () => void
href?: string
}
export let dropdownItems: MenuItem[] | undefined = undefined
const dispatch = createEventDispatcher()
// Order of classes: border, border modifier, bg, bg modifier, text, text modifier, everything else
const colorVariants: Record<ButtonType.Color, Record<ButtonType.Variant, string>> = {
@@ -76,8 +84,8 @@
variant === 'border' ? 'border' : '',
ButtonType.FontSizeClasses[size],
ButtonType.SpacingClasses[spacingSize][variant],
'focus:ring-2 font-semibold',
'rounded-md',
'focus:ring-2 font-semibold h-full',
dropdownItems ? 'rounded-l-md' : 'rounded-md',
'justify-center items-center text-center whitespace-nowrap inline-flex',
btnClasses,
disabled ? '!bg-gray-300 !text-gray-600 !cursor-not-allowed' : '',
@@ -113,28 +121,46 @@
$: endIconClass = twMerge(iconOnly ? undefined : isSmall ? 'ml-1' : 'ml-2', endIcon?.classes)
</script>
<svelte:element
this={href ? 'a' : 'button'}
bind:this={element}
on:pointerdown
on:click={onClick}
on:focus
on:blur
{...buttonProps}
disabled={disabled || loading}
type="submit"
{style}
>
{#if loading}
<Loader2 class="animate-spin mr-1" size={14} />
{:else if startIcon}
<Icon data={startIcon.icon} class={startIconClass} scale={ButtonType.IconScale[size]} />
{/if}
<div class="flex flex-row divide-x divide-frost-600">
<svelte:element
this={href ? 'a' : 'button'}
bind:this={element}
on:pointerdown
on:click={onClick}
on:focus
on:blur
{...buttonProps}
disabled={disabled || loading}
type="submit"
{style}
>
{#if loading}
<Loader2 class="animate-spin mr-1" size={14} />
{:else if startIcon}
<Icon data={startIcon.icon} class={startIconClass} scale={ButtonType.IconScale[size]} />
{/if}
{#if !iconOnly}
<slot />
{#if !iconOnly}
<slot />
{/if}
{#if endIcon}
<Icon data={endIcon.icon} class={endIconClass} scale={ButtonType.IconScale[size]} />
{/if}
</svelte:element>
{#if dropdownItems}
<div class={twMerge(buttonProps.class, 'rounded-r-md rounded-l-none m-0 p-0 h-auto')}>
<ButtonDropdown>
<svelte:fragment slot="items">
{#each dropdownItems as item}
<MenuItem on:click={item.onClick} href={item.href}>
<div class="!text-gray-700 px-4 py-2 my-1 cursor-pointer hover:bg-gray-100 !text-sm">
{item.label}
</div>
</MenuItem>
{/each}
</svelte:fragment>
</ButtonDropdown>
</div>
{/if}
{#if endIcon}
<Icon data={endIcon.icon} class={endIconClass} scale={ButtonType.IconScale[size]} />
{/if}
</svelte:element>
</div>
@@ -0,0 +1,25 @@
<script>
import { Menu, Transition, MenuButton, MenuItems } from '@rgossiaux/svelte-headlessui'
import { ChevronDown } from 'lucide-svelte'
</script>
<Menu let:open as="div" class="relative z-50 flex w-full h-full">
<MenuButton class="h-full w-full px-2">
<ChevronDown class="w-5 h-5" />
</MenuButton>
<Transition
show={open}
enter="transition ease-out duration-25"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-25"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems
class="absolute right-0 z-50 mt-2 w-56 origin-top-right top-8 divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<slot name="items" />
</MenuItems>
</Transition>
</Menu>
@@ -1,81 +0,0 @@
<script lang="ts">
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
import { setContext } from 'svelte'
import Icon from 'svelte-awesome'
import { Button, ButtonType, Popup } from '..'
export let size: ButtonType.Size = 'md'
export let color: ButtonType.Color = 'blue'
export let variant: ButtonType.Variant = 'contained'
export let mainClasses: string = ''
export let toggleClasses: string = ''
export let listClasses: string = ''
export let disabled: boolean = false
export let href: string | undefined = undefined
export let target: ButtonType.Target = '_self'
export let startIcon: ButtonType.Icon | undefined = undefined
export let endIcon: ButtonType.Icon | undefined = undefined
export let spacingSize: ButtonType.Size = size
export let loading = false
let ref: ButtonType.Element
setContext<ButtonType.ItemProps>(ButtonType.ItemContextKey, { size, color })
$: separator = color === 'red' || color === 'blue' ? 'border-frost-600' : 'border-gray-400'
</script>
<div class="flex justy-start items-center">
{#if $$slots.main}
<Button
{loading}
{size}
{color}
{variant}
{disabled}
{spacingSize}
{href}
{target}
{startIcon}
{endIcon}
btnClasses="!rounded-r-none !border-r-0 {mainClasses}"
on:click
>
<slot name="main" />
</Button>
{/if}
<span class={$$slots.main && variant === 'contained' ? 'border-l ' + separator : ''}>
<Button
bind:element={ref}
{size}
{color}
{variant}
{disabled}
{spacingSize}
btnClasses="{$$slots.main ? '!rounded-l-none !ring-0' : ''} {toggleClasses}"
on:click={() => {}}
>
<slot name="toggle">
<!-- Invisible, but needed to match the height of the 'main' button -->
<span class="!opacity-0 !w-0">A</span>
<Icon data={faChevronDown} scale={ButtonType.IconScale[size]} />
</slot>
</Button>
</span>
</div>
{#if ref}
<Popup
{ref}
let:open
let:close
options={{
placement: $$slots.main ? 'bottom-end' : 'bottom',
strategy: 'absolute',
modifiers: [{ name: 'offset', options: { offset: [0, 5] } }]
}}
>
<ul class="bg-white rounded-sm border shadow pt-1 pb-2 max-h-40 overflow-auto {listClasses}">
<slot {open} {close} />
</ul>
</Popup>
{/if}
@@ -1,53 +0,0 @@
<script lang="ts">
import { getContext } from 'svelte'
import { Button, ButtonType } from '..'
import { classNames } from '../../../utils'
export let btnClasses: string = ''
export let disabled: boolean = false
export let href: string | undefined = undefined
export let target: ButtonType.Target = '_self'
export let iconOnly: boolean = false
export let startIcon: ButtonType.Icon | undefined = undefined
export let endIcon: ButtonType.Icon | undefined = undefined
export let wrapperClasses = ''
const props = getContext<ButtonType.ItemProps | undefined>(ButtonType.ItemContextKey)
const iconWidthClass: Record<ButtonType.Size, string> = {
xs: '!w-[12px]',
sm: '!w-[14px]',
md: '!w-[16px]',
lg: '!w-[18px]',
xl: '!w-[20px]'
}
const getWidthClass = () => (props?.size ? iconWidthClass[props.size] : undefined)
$: buttonProps = {
...props,
variant: <ButtonType.Variant>'border',
btnClasses: classNames(btnClasses, '!justify-start !border-0 !rounded-none !w-full'),
disabled,
href,
target,
iconOnly,
startIcon: startIcon
? {
icon: startIcon.icon,
classes: classNames(startIcon?.classes, getWidthClass())
}
: undefined,
endIcon: endIcon
? {
icon: endIcon.icon,
classes: classNames(endIcon?.classes, getWidthClass())
}
: undefined
}
</script>
<li class="mt-1 {wrapperClasses}">
<Button {...buttonProps} on:click>
<slot />
</Button>
</li>
@@ -1,9 +1,8 @@
export const BUTTON_COLORS = ['blue', 'red', 'dark', 'light', 'green', 'gray', 'none'] as const
export namespace ButtonType {
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export type Color = typeof BUTTON_COLORS[number]
export type Color = (typeof BUTTON_COLORS)[number]
export type Variant = 'contained' | 'border'
export type Target = '_self' | '_blank'
export type Element = HTMLButtonElement | HTMLAnchorElement
@@ -40,7 +39,7 @@ export namespace ButtonType {
xl: {
border: 'px-4 py-[8px]',
contained: 'px-4 py-[9px]'
},
}
} as const
export const IconScale: Record<ButtonType.Size, number> = {
@@ -51,10 +50,6 @@ export namespace ButtonType {
xl: 1.2
} as const
// ButtonPopup types
export const ItemContextKey = 'popupItemProps' as const
export interface ItemProps {
size: Size
color: Color
@@ -2,8 +2,6 @@ export { default as ActionRow } from './actionRow/ActionRow.svelte'
export { default as Alert } from './alert/Alert.svelte'
export { default as Badge } from './badge/Badge.svelte'
export { default as Button } from './button/Button.svelte'
export { default as ButtonPopup } from './button/ButtonPopup.svelte'
export { default as ButtonPopupItem } from './button/ButtonPopupItem.svelte'
export { default as UndoRedo } from './button/UndoRedo.svelte'
export { default as ClearableInput } from './clearableInput/ClearableInput.svelte'
export { default as Drawer } from './drawer/Drawer.svelte'
@@ -2,7 +2,7 @@
import { goto } from '$app/navigation'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { Button, ButtonPopup, ButtonPopupItem } from '$lib/components/common'
import { Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
@@ -21,17 +21,22 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<ButtonPopup
<Button
size="sm"
spacingSize="xl"
startIcon={{ icon: faPlus }}
href="/apps/add?nodraft=true"
dropdownItems={[
{
label: 'Import from raw JSON',
onClick: () => drawer?.toggleDrawer?.()
}
]}
>
<svelte:fragment slot="main">App <LayoutDashboard class="ml-1.5" size={18} /></svelte:fragment>
<ButtonPopupItem on:click={() => drawer?.toggleDrawer?.()}>
Import from raw JSON
</ButtonPopupItem>
</ButtonPopup>
<div class="flex flex-row">
App <LayoutDashboard class="ml-1.5" size={18} />
</div>
</Button>
</div>
<!-- Raw JSON -->
@@ -2,12 +2,11 @@
import { goto } from '$app/navigation'
import { faBarsStaggered, faPlus } from '@fortawesome/free-solid-svg-icons'
import { Button, ButtonPopup, ButtonPopupItem } from '$lib/components/common'
import { Button } from '$lib/components/common'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { importFlowStore } from '$lib/components/flows/flowStore'
import { Icon } from 'svelte-awesome'
let drawer: Drawer | undefined = undefined
let pendingJson: string
@@ -21,19 +20,21 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<ButtonPopup
<Button
size="sm"
spacingSize="xl"
startIcon={{ icon: faPlus }}
endIcon={{ icon: faBarsStaggered }}
href="/flows/add?nodraft=true"
dropdownItems={[
{
label: 'Import from raw JSON',
onClick: () => drawer?.toggleDrawer?.()
}
]}
>
<svelte:fragment slot="main"
>Flow <Icon data={faBarsStaggered} scale={0.8} class="ml-1.5" />
</svelte:fragment>
<ButtonPopupItem on:click={() => drawer?.toggleDrawer?.()}>
Import from raw JSON
</ButtonPopupItem>
</ButtonPopup>
Flow
</Button>
</div>
<!-- Raw JSON -->
@@ -7,13 +7,7 @@
<!-- Buttons -->
<div class="flex flex-row gap-2">
<Button size="sm" spacingSize="xl" startIcon={{ icon: faPlus }} href="/scripts/add"
>Script <Code2 class="ml-1.5" size={18} /></Button
>
<!-- <ButtonPopup size="sm" spacingSize="xl" startIcon={{ icon: faPlus }} href="/scripts/add">
<svelte:fragment slot="main">Script <Code2 class="ml-1.5" size={18} /></svelte:fragment>
<ButtonPopupItem on:click={() => drawer?.toggleDrawer?.()}>
Import from template
</ButtonPopupItem>
</ButtonPopup> -->
<Button size="sm" spacingSize="xl" startIcon={{ icon: faPlus }} href="/scripts/add">
Script <Code2 class="ml-1.5" size={18} />
</Button>
</div>
@@ -243,7 +243,7 @@
}}
/>
</div>
<div class="flex flex-row gap-x-2 w-full mb-2">
<div class="flex flex-row gap-x-2 w-full mb-2 mt-4">
<div class="relative w-full"
><span class="text-xs absolute -top-4">min datetime</span>
<input type="text" value={minTs ?? 'zoom x axis to set min (drag with ctrl)'} disabled />
@@ -35,16 +35,7 @@
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { onDestroy } from 'svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import {
Badge,
Tabs,
Tab,
TabContent,
Button,
Alert,
ButtonPopup,
ButtonPopupItem
} from '$lib/components/common'
import { Badge, Tabs, Tab, TabContent, Button, Alert } from '$lib/components/common'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import UserSettings from '$lib/components/UserSettings.svelte'
import Icon from 'svelte-awesome'
@@ -333,26 +324,19 @@
on:click={() => webhookElem.scrollIntoView()}>Webhooks</Button
>
{#if Array.isArray(script.parent_hashes) && script.parent_hashes.length > 0}
<ButtonPopup
<Button
color="dark"
variant="contained"
size="xs"
startIcon={{ icon: faArrowLeft }}
href="/scripts/get/{script.parent_hashes[0]}?workspace_id={$workspaceStore}"
dropdownItems={script.parent_hashes.map((hash) => ({
href: `/scripts/get/${hash}?workspace_id={$workspaceStore}`,
label: hash
}))}
>
<svelte:fragment slot="main">
Previous version ({script.parent_hashes.length})
</svelte:fragment>
{#each script.parent_hashes as hash}
<ButtonPopupItem
href="/scripts/get/{hash}?workspace_id={$workspaceStore}"
btnClasses="!m-0"
>
{hash}
</ButtonPopupItem>
{/each}
</ButtonPopup>
Previous version ({script.parent_hashes.length})
</Button>
{/if}
</div>