feat(frontend): Button with popup (#639)

* feat(frontend): Add ButtonPopup component
This commit is contained in:
Ádám Kovács
2022-10-06 14:18:42 +02:00
committed by GitHub
parent 8b60e9154c
commit 70f6fc24ad
16 changed files with 611 additions and 130 deletions
@@ -1,29 +1,31 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { goto } from '$app/navigation'
import { classNames } from '$lib/utils'
import Icon from 'svelte-awesome'
import type { Button } from './model'
import { ButtonType } from './model'
export let size: Button.Size = 'md'
export let spacingSize: Button.Size = size
export let color: Button.Color = 'blue'
export let variant: Button.Variant = 'contained'
export let size: ButtonType.Size = 'md'
export let spacingSize: ButtonType.Size = size
export let color: ButtonType.Color = 'blue'
export let variant: ButtonType.Variant = 'contained'
export let btnClasses: string = ''
export let disabled: boolean = false
export let href: string | undefined = undefined
export let target: Button.Target = '_self'
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 element: ButtonType.Element | undefined = undefined
export let id: string = ''
export let startIcon: { icon: any; classes?: string } | undefined = undefined
export let endIcon: { icon: any; classes?: string } | undefined = undefined
const dispatch = createEventDispatcher()
// Order of classes: border, border modifier, bg, bg modifier, text, text modifier, everything else
const colorVariants: Record<Button.Color, Record<Button.Variant, string>> = {
const colorVariants: Record<ButtonType.Color, Record<ButtonType.Variant, string>> = {
blue: {
border:
'border-blue-500 hover:border-blue-700 bg-white hover:bg-blue-100 text-blue-500 hover:text-blue-700 focus:ring-blue-300',
contained: 'bg-blue-500 hover:bg-blue-700 text-white focus:ring-blue-300'
'border-blue-500 hover:border-blue-700 focus:border-blue-700 bg-white hover:bg-blue-100 focus:bg-blue-100 text-blue-500 hover:text-blue-700 focus:text-blue-700 focus:ring-blue-300',
contained: 'bg-blue-500 hover:bg-blue-700 focus:bg-blue-700 text-white focus:ring-blue-300'
},
red: {
border:
@@ -32,102 +34,62 @@
},
dark: {
border:
'border-gray-800 hover:border-gray-900 bg-white hover:bg-gray-200 text-gray-800 hover:text-gray-900 focus:ring-gray-300',
contained: 'bg-gray-700 hover:bg-gray-900 text-white focus:ring-gray-300'
'border-gray-800 hover:border-gray-900 focus:border-gray-900 bg-white hover:bg-gray-200 focus:bg-gray-200 text-gray-800 hover:text-gray-900 focus:text-gray-900 focus:ring-gray-300',
contained: 'bg-gray-700 hover:bg-gray-900 focus:bg-gray-900 text-white focus:ring-gray-300'
},
light: {
border:
'border bg-white hover:bg-gray-100 text-gray-700 hover:text-gray-800 focus:ring-gray-300',
contained: 'bg-white hover:bg-gray-100 text-gray-700 focus:ring-gray-300'
'border bg-white hover:bg-gray-100 focus:bg-gray-100 text-gray-700 hover:text-gray-800 focus:text-gray-800 focus:ring-gray-300',
contained: 'bg-white hover:bg-gray-100 focus:bg-gray-100 text-gray-700 focus:ring-gray-300'
}
}
const fontSizeClasses: Record<Button.Size, string> = {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-md',
lg: 'text-lg',
xl: 'text-xl'
}
const spacingClasses: Record<Button.Size, string> = {
xs: 'px-3 py-1.5',
sm: 'px-3 py-1.5',
md: 'px-4 py-2',
lg: 'px-4 py-2',
xl: 'px-4 py-2'
}
const iconScale: Record<Button.Size, number> = {
xs: 0.6,
sm: 0.8,
md: 1,
lg: 1.1,
xl: 1.2
}
$: buttonProps = {
class: classNames(
colorVariants[color][variant],
variant === 'border' ? 'border' : '',
fontSizeClasses[size],
spacingClasses[spacingSize],
ButtonType.FontSizeClasses[size],
ButtonType.SpacingClasses[spacingSize],
'focus:ring-4 font-medium',
'rounded-md',
'flex justify-center items-center text-center whitespace-nowrap',
btnClasses,
disabled ? 'pointer-events-none cursor-default filter grayscale' : ''
),
disabled
disabled,
href,
target,
tabindex: disabled ? -1 : 0
}
function onClick(event: MouseEvent) {
dispatch('click', event)
if (href) goto(href)
}
$: isSmall = size === 'xs' || size === 'sm'
$: startIconClass = classNames(
iconOnly ? undefined : isSmall ? 'mr-1' : 'mr-2',
startIcon?.classes
)
$: endIconClass = classNames(iconOnly ? undefined : isSmall ? 'ml-1' : 'ml-2', endIcon?.classes)
</script>
{#if href}
<button
{id}
type="button"
on:click|stopPropagation={() => goto(href ?? '#')}
{target}
tabindex={disabled ? -1 : 0}
{...buttonProps}
>
{#if startIcon}
<Icon
data={startIcon.icon}
class={classNames(iconOnly ? undefined : 'mr-2', startIcon.classes)}
scale={iconScale[size]}
/>
{/if}
{#if !iconOnly}
<slot />
{/if}
{#if endIcon}
<Icon
data={endIcon.icon}
class={classNames(iconOnly ? undefined : 'ml-2', endIcon.classes)}
scale={iconScale[size]}
/>
{/if}
</button>
{:else}
<button {id} type="button" on:click|stopPropagation {...buttonProps} {...$$restProps}>
{#if startIcon}
<Icon
data={startIcon.icon}
class={classNames(iconOnly ? undefined : 'mr-2', startIcon.classes)}
scale={iconScale[size]}
/>
{/if}
{#if !iconOnly}
<slot />
{/if}
{#if endIcon}
<Icon
data={endIcon.icon}
class={classNames(iconOnly ? undefined : 'ml-2', endIcon.classes)}
scale={iconScale[size]}
/>
{/if}
</button>
{/if}
<svelte:element
this={href ? 'a' : 'button'}
bind:this={element}
on:click|stopPropagation={onClick}
on:focus
on:blur
{...buttonProps}
>
{#if startIcon}
<Icon data={startIcon.icon} class={startIconClass} scale={ButtonType.IconScale[size]} />
{/if}
{#if !iconOnly}
<slot />
{/if}
{#if endIcon}
<Icon data={endIcon.icon} class={endIconClass} scale={ButtonType.IconScale[size]} />
{/if}
</svelte:element>
@@ -0,0 +1,72 @@
<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 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
let ref: ButtonType.Element
setContext<ButtonType.ItemProps>(ButtonType.ItemContextKey, { size, color })
$: separator = color === 'red' || color === 'blue' ? 'border-gray-200' : 'border-gray-400'
$: commonProps = {
size,
color,
variant,
disabled
}
</script>
<div class="flex justy-start items-center">
{#if $$slots.main}
<Button
{...commonProps}
{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}
{...commonProps}
btnClasses="{$$slots.main ? '!rounded-l-none' : ''} {toggleClasses}"
>
<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}
options={{
placement: $$slots.main ? 'bottom-end' : 'bottom',
strategy: 'absolute',
modifiers: [{ name: 'offset', options: { offset: [0, 0] } }]
}}
>
<ul class="bg-white rounded-t border pt-1 pb-2 max-h-40 overflow-auto">
<slot />
</ul>
</Popup>
{/if}
@@ -0,0 +1,52 @@
<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
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">
<Button {...buttonProps} on:click>
<slot />
</Button>
</li>
@@ -1,6 +1,44 @@
export namespace Button {
export namespace ButtonType {
export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export type Color = 'blue' | 'red' | 'dark' | 'light'
export type Variant = 'contained' | 'border'
export type Target = '_self' | '_blank'
export type Element = HTMLButtonElement | HTMLAnchorElement
export interface Icon {
icon: any
classes?: string
}
export const FontSizeClasses: Record<ButtonType.Size, string> = {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-md',
lg: 'text-lg',
xl: 'text-xl'
} as const
export const SpacingClasses: Record<ButtonType.Size, string> = {
xs: 'px-3 py-1.5',
sm: 'px-3 py-1.5',
md: 'px-4 py-2',
lg: 'px-4 py-2',
xl: 'px-4 py-2'
} as const
export const IconScale: Record<ButtonType.Size, number> = {
xs: 0.7,
sm: 0.8,
md: 1,
lg: 1.1,
xl: 1.2
} as const
// ButtonPopup types
export const ItemContextKey = 'popupItemProps' as const
export interface ItemProps {
size: Size
color: Color
}
}
@@ -31,6 +31,8 @@
open = !open
}
$: open ? dispatch('open') : dispatch('close')
onMount(() => {
mounted = true
scrollLock(open)
@@ -7,7 +7,7 @@
const dispatch = createEventDispatcher()
</script>
<div class="divide-y h-screen">
<div class="flex flex-col divide-y h-screen">
<div class="flex justify-between items-center py-2 px-4">
<span class="text-sm font-bold">{title}</span>
<button on:click={() => dispatch('close')}>
@@ -1,10 +1,14 @@
export { default as ActionRow } from './actionRow/ActionRow.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 Drawer } from './drawer/Drawer.svelte'
export { default as DrawerContent } from './drawer/DrawerContent.svelte'
export { default as Kbd } from './kbd/Kbd.svelte'
export { default as Menu } from './menu/Menu.svelte'
export { default as MenuItem } from './menu/MenuItem.svelte'
export { default as Popup } from './popup/Popup.svelte'
export { default as Tab } from './tabs/Tab.svelte'
export { default as TabContent } from './tabs/TabContent.svelte'
export { default as Tabs } from './tabs/Tabs.svelte'
@@ -0,0 +1,7 @@
<kbd
class="mx-1 px-2 py-1.5 text-xs font-semibold text-gray-800
bg-gray-100 border border-gray-200 rounded-lg
dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500 {$$props.class}"
>
<slot />
</kbd>
@@ -0,0 +1,161 @@
<svelte:options accessors />
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { slide } from 'svelte/transition'
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
import { clickOutside } from '../../../utils'
import { Kbd } from '..'
import { createStateMachine } from '../../../stateMachine'
export let ref: HTMLElement
export let options: PopperOptions<any> = { placement: 'auto' }
/** Events on the reference element */
export let openOn: (keyof HTMLElementEventMap)[] = ['focus']
/** Events on the reference element */
export let closeOn: (keyof HTMLElementEventMap)[] = ['blur']
export let disableInstruction = false
export let innerClasses = ''
export let outerClasses = ''
const states = ['closed', 'open-focus-in', 'open-focus-out'] as const
const stateMachine = createStateMachine(states, {
to: {
closed: ({ previousState, currentState }) => {
const activeElem = document.activeElement
const revert = popup.contains(activeElem) || ref.contains(activeElem)
return revert ? previousState : currentState
}
}
})
const [popperRef, popperContent] = createPopperActions()
let popup: HTMLElement
let focusableElements: HTMLElement[]
function getFocusableElements() {
let elements: HTMLElement[] = []
popup
.querySelectorAll<HTMLElement>(
'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
)
.forEach((elem) => elements.push(elem))
focusableElements = elements.filter(
(el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')
)
focusableElements.forEach((el) => {
el.tabIndex = -1
el.addEventListener('click', openFocusIn)
el.addEventListener('blur', closed)
})
}
function closed() {
if ($stateMachine.currentState === 'open-focus-out') {
setTimeout(() => {
stateMachine.setState('closed')
}, 0)
} else {
stateMachine.setState('closed')
}
}
function openFocusOut() {
stateMachine.setState('open-focus-out')
}
function openFocusIn() {
stateMachine.setState('open-focus-in')
}
function keyDown(event: KeyboardEvent & { currentTarget: EventTarget & Window }) {
const modifiers = ['Shift', 'Control', 'Command', 'Alt']
// Prevent closing the popup when the only key pressed is a modifier key
if (modifiers.includes(event.key) || $stateMachine.currentState === 'closed') return
if (event.key === 'Escape') {
return (<HTMLElement>document.activeElement)?.blur()
}
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
event.preventDefault()
if (popup.contains(document.activeElement)) {
const index = focusableElements.findIndex((elem) => elem === document.activeElement)
if (index === -1) return
let targetIndex: number | undefined = undefined
if (event.key === 'ArrowUp') {
targetIndex = index === 0 ? focusableElements.length - 1 : index - 1
} else if (event.key === 'ArrowDown') {
targetIndex = index + 1 === focusableElements.length ? 0 : index + 1
}
if (targetIndex !== undefined) {
focusableElements[targetIndex].focus()
stateMachine.setState('open-focus-in')
}
} else {
const elem = focusableElements[event.key === 'ArrowUp' ? focusableElements.length - 1 : 0]
if (elem) {
elem.focus()
stateMachine.setState('open-focus-in')
}
}
}
function addRefListeners() {
openOn.forEach((action) => ref.addEventListener(action, openFocusOut))
closeOn.forEach((action) => ref.addEventListener(action, closed))
}
function removeAllListeners() {
focusableElements?.forEach((el) => el.removeEventListener('click', openFocusIn))
focusableElements?.forEach((el) => el.removeEventListener('blur', closed))
openOn.forEach((action) => ref.removeEventListener(action, openFocusOut))
closeOn.forEach((action) => ref.removeEventListener(action, closed))
}
$: if ($stateMachine.currentState === 'closed') {
focusableElements?.forEach((el) => el.removeEventListener('click', openFocusIn))
} else {
setTimeout(() => {
getFocusableElements()
}, 0)
}
onMount(() => {
popperRef(ref)
addRefListeners()
})
onDestroy(removeAllListeners)
</script>
<svelte:window on:keydown={keyDown} />
<div
bind:this={popup}
use:popperContent={options}
use:clickOutside
on:click_outside={closed}
aria-haspopup="true"
aria-expanded={$stateMachine.currentState !== 'closed'}
>
{#if $stateMachine.currentState !== 'closed'}
<div transition:slide={{ duration: 200 }} class={outerClasses}>
<div class={innerClasses}>
<slot />
</div>
{#if !disableInstruction && focusableElements?.length}
<div
class="flex justify-center items-center font-semibold
text-xs text-gray-700 p-1 border-x border-b rounded-b bg-gray-100"
>
Use
<Kbd class="!bg-gray-200 !border-gray-300 !px-1 !py-0 !rounded-sm"></Kbd>
and
<Kbd class="!bg-gray-200 !border-gray-300 !px-1 !py-0 !rounded-sm"></Kbd>
</div>
{/if}
</div>
{/if}
</div>
@@ -85,8 +85,8 @@
variant="border"
startIcon={{ icon: faTrashAlt }}
{iconOnly}
on:click={(event) => {
if (event.shiftKey || shouldPick) {
on:click={({ detail }) => {
if (detail.shiftKey || shouldPick) {
dispatch('delete')
select('settings')
} else {
@@ -6,13 +6,12 @@
import { hubScripts } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import { Script } from '$lib/gen'
import type { HubItem } from './model'
export let kind: Script.kind
type Item = { summary: String; path: String; version?: String }
let items: Item[] | undefined
$: items = $hubScripts?.filter((x) => x.kind == kind)
let items: HubItem[]
$: items = $hubScripts?.filter((x) => x.kind == kind) ?? []
let itemPicker: ItemPicker
const dispatch = createEventDispatcher()
@@ -0,0 +1,5 @@
export interface HubItem {
summary: String
path: String
version?: String
}
@@ -0,0 +1,126 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import Fuse from 'fuse.js'
import { Script } from '$lib/gen'
import { ScriptService } from '$lib/gen'
import { workspaceStore, hubScripts } from '$lib/stores'
import { sendUserToast } from '$lib/utils'
import { Button, ButtonPopup, ButtonPopupItem } from '$lib/components/common'
import ItemPicker from '../ItemPicker.svelte'
import type { HubItem } from '../flows/pickers/model'
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 { flowStore, initFlow } from '$lib/components/flows/flowStore'
const drawers: {
hub: ItemPicker | undefined
template: Drawer | undefined
json: Drawer | undefined
} = {
hub: undefined,
template: undefined,
json: undefined
}
let hubItems: HubItem[]
let pendingJson: string
let templateScripts: Script[] = []
let templateFilter = ''
let filteredTemplates: Script[] | undefined
const fuseOptions = {
includeScore: false,
keys: ['description', 'path', 'content', 'hash', 'summary']
}
const templateFuse: Fuse<Script> = new Fuse(templateScripts, fuseOptions)
$: hubItems = $hubScripts?.filter((x) => x.kind == Script.kind.SCRIPT) || []
$: filteredTemplates =
templateFilter.length > 0
? templateFuse.search(templateFilter).map((value) => value.item)
: templateScripts
function importJson() {
Object.assign($flowStore, JSON.parse(pendingJson))
initFlow($flowStore)
sendUserToast('OpenFlow imported from JSON')
drawers.json?.toggleDrawer()
}
async function loadTemplateScripts(): Promise<void> {
templateScripts = await ScriptService.listScripts({
workspace: $workspaceStore!,
isTemplate: true
})
templateFuse.setCollection(templateScripts)
}
</script>
<!-- Buttons -->
<div class="flex flex-row gap-2">
<ButtonPopup size="sm" startIcon={{ icon: faPlus }} href="/scripts/add">
<svelte:fragment slot="main">New script</svelte:fragment>
<ButtonPopupItem on:click={() => drawers.hub?.openModal()}>
Import script from WindmillHub
</ButtonPopupItem>
<ButtonPopupItem on:click={() => drawers.template?.toggleDrawer()}>
Import script from template
</ButtonPopupItem>
<ButtonPopupItem on:click={() => drawers.json?.toggleDrawer()}>
Import script from raw JSON
</ButtonPopupItem>
</ButtonPopup>
</div>
<!-- Initially hidden elements in a drawer -->
<!-- WindmillHub script list -->
<ItemPicker
bind:this={drawers.hub}
pickCallback={(path) => {
console.log('pick', { path })
goto('/scripts/add?hub=' + path)
}}
itemName={'Script'}
extraField="summary"
loadItems={async () => {
return hubItems
}}
/>
<!-- Template script list -->
<Drawer bind:this={drawers.template} size="800px" on:open={loadTemplateScripts}>
<DrawerContent title="Pick a template" on:close={() => drawers.template?.toggleDrawer()}>
<div class="pt-2 pb-4">
<input placeholder="Search templates" bind:value={templateFilter} class="search-bar" />
</div>
<div class="flex flex-col mb-2 md:mb-6">
{#if filteredTemplates && filteredTemplates.length > 0}
{#each filteredTemplates as { summary, path, hash }}
<a
class="p-1 flex flex-row items-baseline gap-2 selected text-gray-700"
href="/scripts/add?template={path}"
>
{#if summary}
<p class="text-sm font-semibold">{summary}</p>
{/if}
<p class="text-sm">{path}</p>
<p class="text-gray-400 text-xs text-right grow">
Last version: {hash}
</p>
</a>
{/each}
{:else}
<p class="text-sm text-gray-700">No templates</p>
{/if}
</div>
</DrawerContent>
</Drawer>
<!-- Raw JSON -->
<Drawer bind:this={drawers.json} size="800px">
<DrawerContent title="Import JSON" on:close={() => drawers.json?.toggleDrawer()}>
<div class="p-2"><Button size="sm" on:click={importJson}>Import</Button></div>
<SimpleEditor bind:code={pendingJson} lang="json" class="h-full" />
</DrawerContent>
</Drawer>
+77
View File
@@ -0,0 +1,77 @@
import { writable, type Readable } from 'svelte/store'
export interface StateMachine<T extends readonly string[]> {
states: T
currentState: T[number]
}
export interface StateMachineTransition<T extends readonly string[]> {
from?: Partial<Record<T[number], TransitionFromFunction<T>>>
to?: Partial<Record<T[number], TransitionToFunction<T>>>
}
type StateMachineInfo<T extends readonly string[]> = {
states: StateMachine<T>['states']
currentState: StateMachine<T>['currentState']
}
/** The return value should be a `state` that is available on the current machine. */
export type TransitionFromFunction<T extends readonly string[]> = (
info: StateMachineInfo<T> & { desiredState: T[number] }
) => T[number]
/** Callback after the state has been changed. */
export type TransitionToFunction<T extends readonly string[]> = (
info: StateMachineInfo<T> & { previousState: T[number] }
) => T[number]
type StateStore<T extends readonly string[]> = Readable<StateMachine<T>> & {
setState: (state: T[number]) => StateMachineInfo<T>
}
/** **IMPORTANT:** use the `as const` syntax on the states array to get type safety.
* *Example: `createStateMachine(['foo', 'bar'] as const)`*
*
* Returns a new state machine with the default state set to the first element of the `states` argument. */
export function createStateMachine<T extends readonly string[]>(
states: T,
transition: StateMachineTransition<T> = {}
): StateStore<T> {
const defaultValue: StateMachine<T> = {
states,
currentState: states[0]
}
const defaultStore = writable(defaultValue)
const stateStore: StateStore<T> = {
subscribe: defaultStore.subscribe,
setState: (nextState) => {
defaultStore.update((prev) => {
const previousState = prev.currentState
const beforeFunc = transition?.from && transition.from[previousState]
const afterFunc = transition?.to && transition.to[nextState]
let returnState = nextState
if (beforeFunc) {
returnState = beforeFunc({
states,
currentState: previousState,
desiredState: nextState
})
}
if (afterFunc) {
returnState = afterFunc({
states,
currentState: returnState,
previousState
})
}
prev.currentState = returnState
return prev
})
return { states, currentState: nextState }
}
}
return stateStore
}
+4 -5
View File
@@ -99,11 +99,10 @@ export function validatePassword(password: string): boolean {
return re.test(password)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function clickOutside(node: any): any {
const handleClick = (event: Event) => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
node.dispatchEvent(new CustomEvent('click_outside', node))
export function clickOutside(node: Node): { destroy(): void } {
const handleClick = (event: MouseEvent) => {
if (node && !node.contains(<HTMLElement>event.target) && !event.defaultPrevented) {
node.dispatchEvent(new CustomEvent<MouseEvent>('click_outside', { detail: event }))
}
}
+4 -27
View File
@@ -15,11 +15,9 @@
faEye,
faList,
faPlay,
faPlus,
faShare
} from '@fortawesome/free-solid-svg-icons'
import Fuse from 'fuse.js'
import Icon from 'svelte-awesome'
import type { Script } from '$lib/gen'
import { ScriptService } from '$lib/gen'
import { superadmin, userStore, workspaceStore, hubScripts } from '$lib/stores'
@@ -44,6 +42,7 @@
import { Highlight } from 'svelte-highlight'
import { typescript } from 'svelte-highlight/languages/typescript'
import { Button } from '$lib/components/common'
import CreateActions from '$lib/components/scripts/CreateActions.svelte'
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'examples' | 'hub'
type Section = [string, ScriptW[]]
@@ -55,7 +54,6 @@
let groupedScripts: Section[] = []
let communityScripts: Section[] = []
let templateModal: Modal
let templateScripts: Script[] = []
let templateFilter = ''
let filteredTemplates: Script[] | undefined
@@ -110,14 +108,6 @@
communityScripts = [['examples', filteredScripts.filter((x) => x.tab == 'examples')]]
}
async function loadTemplateScripts(): Promise<void> {
templateScripts = await ScriptService.listScripts({
workspace: $workspaceStore!,
isTemplate: true
})
templateFuse.setCollection(templateScripts)
}
function tabFromPath(path: string) {
let t: Tab = 'shared'
let path_prefix = path.split('/').slice(0, 2)
@@ -187,19 +177,7 @@
granted visibility on the resources and variables it uses, otherwise it will behave as if those
items did not exist at runtime of the script."
>
<div class="flex flex-row gap-2">
<Button
variant="border"
size="sm"
startIcon={{ icon: faPlus }}
on:click={() => {
templateModal.openModal()
}}
>
New script from template
</Button>
<Button size="sm" startIcon={{ icon: faPlus }} href="/scripts/add">New script</Button>
</div>
<CreateActions />
</PageHeader>
<Tabs
@@ -452,7 +430,7 @@
}}
/>
<Modal
<!-- <Modal
bind:this={templateModal}
on:open={() => {
loadTemplateScripts()
@@ -485,8 +463,7 @@
{/if}
</div>
</div>
</Modal>
</Modal> -->
<style>
.selected:hover {
@apply border border-gray-500 rounded-md border-opacity-50;