From 70f6fc24ad308d624cd1da2ea3fa85a3454bafb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= <43071496+adam-kov@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:18:42 +0200 Subject: [PATCH] feat(frontend): Button with popup (#639) * feat(frontend): Add ButtonPopup component --- .../components/common/button/Button.svelte | 142 ++++++--------- .../common/button/ButtonPopup.svelte | 72 ++++++++ .../common/button/ButtonPopupItem.svelte | 52 ++++++ .../src/lib/components/common/button/model.ts | 40 ++++- .../components/common/drawer/Drawer.svelte | 2 + .../common/drawer/DrawerContent.svelte | 2 +- frontend/src/lib/components/common/index.ts | 4 + .../src/lib/components/common/kbd/Kbd.svelte | 7 + .../lib/components/common/popup/Popup.svelte | 161 ++++++++++++++++++ .../flows/content/FlowModuleHeader.svelte | 4 +- .../flows/pickers/PickHubScript.svelte | 7 +- .../src/lib/components/flows/pickers/model.ts | 5 + .../components/scripts/CreateActions.svelte | 126 ++++++++++++++ frontend/src/lib/stateMachine.ts | 77 +++++++++ frontend/src/lib/utils.ts | 9 +- frontend/src/routes/scripts.svelte | 31 +--- 16 files changed, 611 insertions(+), 130 deletions(-) create mode 100644 frontend/src/lib/components/common/button/ButtonPopup.svelte create mode 100644 frontend/src/lib/components/common/button/ButtonPopupItem.svelte create mode 100644 frontend/src/lib/components/common/kbd/Kbd.svelte create mode 100644 frontend/src/lib/components/common/popup/Popup.svelte create mode 100644 frontend/src/lib/components/flows/pickers/model.ts create mode 100644 frontend/src/lib/components/scripts/CreateActions.svelte create mode 100644 frontend/src/lib/stateMachine.ts diff --git a/frontend/src/lib/components/common/button/Button.svelte b/frontend/src/lib/components/common/button/Button.svelte index 33447c78e5..da23d59cd6 100644 --- a/frontend/src/lib/components/common/button/Button.svelte +++ b/frontend/src/lib/components/common/button/Button.svelte @@ -1,29 +1,31 @@ -{#if href} - -{:else} - -{/if} + + {#if startIcon} + + {/if} + {#if !iconOnly} + + {/if} + {#if endIcon} + + {/if} + diff --git a/frontend/src/lib/components/common/button/ButtonPopup.svelte b/frontend/src/lib/components/common/button/ButtonPopup.svelte new file mode 100644 index 0000000000..b53594a0c9 --- /dev/null +++ b/frontend/src/lib/components/common/button/ButtonPopup.svelte @@ -0,0 +1,72 @@ + + +
+ {#if $$slots.main} + + {/if} + + + +
+{#if ref} + + + +{/if} diff --git a/frontend/src/lib/components/common/button/ButtonPopupItem.svelte b/frontend/src/lib/components/common/button/ButtonPopupItem.svelte new file mode 100644 index 0000000000..a965e6c292 --- /dev/null +++ b/frontend/src/lib/components/common/button/ButtonPopupItem.svelte @@ -0,0 +1,52 @@ + + +
  • + +
  • diff --git a/frontend/src/lib/components/common/button/model.ts b/frontend/src/lib/components/common/button/model.ts index 1f3a4e808f..4276b24c99 100644 --- a/frontend/src/lib/components/common/button/model.ts +++ b/frontend/src/lib/components/common/button/model.ts @@ -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 = { + xs: 'text-xs', + sm: 'text-sm', + md: 'text-md', + lg: 'text-lg', + xl: 'text-xl' + } as const + + export const SpacingClasses: Record = { + 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 = { + 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 + } } diff --git a/frontend/src/lib/components/common/drawer/Drawer.svelte b/frontend/src/lib/components/common/drawer/Drawer.svelte index ec94a39440..961546ee1f 100644 --- a/frontend/src/lib/components/common/drawer/Drawer.svelte +++ b/frontend/src/lib/components/common/drawer/Drawer.svelte @@ -31,6 +31,8 @@ open = !open } + $: open ? dispatch('open') : dispatch('close') + onMount(() => { mounted = true scrollLock(open) diff --git a/frontend/src/lib/components/common/drawer/DrawerContent.svelte b/frontend/src/lib/components/common/drawer/DrawerContent.svelte index 4bcfcc294c..0a474356fc 100644 --- a/frontend/src/lib/components/common/drawer/DrawerContent.svelte +++ b/frontend/src/lib/components/common/drawer/DrawerContent.svelte @@ -7,7 +7,7 @@ const dispatch = createEventDispatcher() -
    +
    {title}
    + + + diff --git a/frontend/src/lib/stateMachine.ts b/frontend/src/lib/stateMachine.ts new file mode 100644 index 0000000000..cd26e54d5d --- /dev/null +++ b/frontend/src/lib/stateMachine.ts @@ -0,0 +1,77 @@ +import { writable, type Readable } from 'svelte/store' + +export interface StateMachine { + states: T + currentState: T[number] +} + +export interface StateMachineTransition { + from?: Partial>> + to?: Partial>> +} + +type StateMachineInfo = { + states: StateMachine['states'] + currentState: StateMachine['currentState'] +} + +/** The return value should be a `state` that is available on the current machine. */ +export type TransitionFromFunction = ( + info: StateMachineInfo & { desiredState: T[number] } +) => T[number] + +/** Callback after the state has been changed. */ +export type TransitionToFunction = ( + info: StateMachineInfo & { previousState: T[number] } +) => T[number] + +type StateStore = Readable> & { + setState: (state: T[number]) => StateMachineInfo +} + +/** **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( + states: T, + transition: StateMachineTransition = {} +): StateStore { + const defaultValue: StateMachine = { + states, + currentState: states[0] + } + const defaultStore = writable(defaultValue) + const stateStore: StateStore = { + 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 +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 1b3d68feb6..14ad120be4 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -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(event.target) && !event.defaultPrevented) { + node.dispatchEvent(new CustomEvent('click_outside', { detail: event })) } } diff --git a/frontend/src/routes/scripts.svelte b/frontend/src/routes/scripts.svelte index 485860d625..fd60e3160a 100644 --- a/frontend/src/routes/scripts.svelte +++ b/frontend/src/routes/scripts.svelte @@ -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 { - 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." > -
    - - -
    + - { loadTemplateScripts() @@ -485,8 +463,7 @@ {/if}
    - - + -->