feat(frontend): add download button

This commit is contained in:
Ruben Fiszel
2023-05-12 11:08:59 +02:00
parent 4cb3ec5fd3
commit 9b9730d2b7
8 changed files with 197 additions and 5 deletions
@@ -0,0 +1,91 @@
<script lang="ts">
import { getContext } from 'svelte'
import { twMerge } from 'tailwind-merge'
import { initConfig, initOutput } from '../../editor/appUtils'
import { components } from '../../editor/component'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss, transformBareBase64IfNecessary } from '../../utils'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { AlignWrapper } from '../helpers'
import { Button } from '$lib/components/common'
import { loadIcon } from '../icon'
export let id: string
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'downloadcomponent'> | undefined = undefined
export let render: boolean
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let noWFull = false
let resolvedConfig = initConfig(
components['downloadcomponent'].initialData.configuration,
configuration
)
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
let beforeIconComponent: any
let afterIconComponent: any
$: resolvedConfig.beforeIcon && handleBeforeIcon()
$: resolvedConfig.afterIcon && handleAfterIcon()
async function handleBeforeIcon() {
if (resolvedConfig.beforeIcon) {
beforeIconComponent = await loadIcon(resolvedConfig.beforeIcon)
}
}
async function handleAfterIcon() {
if (resolvedConfig.afterIcon) {
afterIconComponent = await loadIcon(resolvedConfig.afterIcon)
}
}
$: css = concatCustomCss($app.css?.downloadcomponent, customCss)
</script>
<InitializeComponent {id} />
{#each Object.keys(components['downloadcomponent'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
{key}
bind:resolvedConfig={resolvedConfig[key]}
configuration={configuration[key]}
/>
{/each}
{#if render}
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<Button
on:pointerdown={(e) => e.stopPropagation()}
btnClasses={twMerge(css?.button?.class, resolvedConfig.fillContainer ? 'w-full h-full' : '')}
wrapperClasses={resolvedConfig.fillContainer ? 'w-full h-full' : ''}
style={css?.button?.style}
disabled={resolvedConfig.source == undefined}
size={resolvedConfig.size}
color={resolvedConfig.color}
download={resolvedConfig.filename}
href={transformBareBase64IfNecessary(resolvedConfig.source)}
nonCaptureEvent
>
<span class="truncate inline-flex gap-2 items-center">
{#if resolvedConfig.beforeIcon && beforeIconComponent}
<svelte:component this={beforeIconComponent} size={14} />
{/if}
{#if resolvedConfig.label && resolvedConfig.label?.length > 0}
<div>{resolvedConfig.label}</div>
{/if}
{#if resolvedConfig.afterIcon && afterIconComponent}
<svelte:component this={afterIconComponent} size={14} />
{/if}
</span>
</Button>
</AlignWrapper>
{/if}
@@ -29,7 +29,7 @@
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
$: css = concatCustomCss($app.css?.containercomponent, customCss)
$: css = concatCustomCss($app.css?.modalcomponent, customCss)
let open = false
function handleKeyUp(event: KeyboardEvent): void {
@@ -95,7 +95,9 @@
<Portal target="#app-editor-top-level-drawer">
<div
class={twMerge(
'fixed top-0 bottom-0 left-0 right-0 transition-all duration-50 overflow-hidden',
`${
$mode == 'dnd' ? 'absolute' : 'fixed'
} top-0 bottom-0 left-0 right-0 transition-all duration-50 overflow-hidden`,
open ? 'z-[1100] bg-black bg-opacity-60' : 'hidden'
)}
>
@@ -44,6 +44,7 @@
import AppStepper from '../../components/layout/AppStepper.svelte'
import AppSelectTab from '../../components/inputs/AppSelectTab.svelte'
import AppConditionalWrapper from '../../components/layout/AppConditionalWrapper.svelte'
import AppDownload from '../../components/display/AppDownload.svelte'
export let component: AppComponent
export let selected: boolean
@@ -239,6 +240,15 @@
bind:errorHandledByComponent
{render}
/>
{:else if component.type === 'downloadcomponent'}
<AppDownload
id={component.id}
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
<AppSelect
id={component.id}
@@ -35,7 +35,8 @@ import {
FlipVertical,
FileText,
AtSignIcon,
Split
Split,
Download
} from 'lucide-svelte'
import type {
Aligned,
@@ -72,6 +73,7 @@ export type VegaLiteComponent = BaseComponent<'vegalitecomponent'>
export type PlotlyComponent = BaseComponent<'plotlycomponent'>
export type TimeseriesComponent = BaseComponent<'timeseriescomponent'>
export type ButtonComponent = BaseComponent<'buttoncomponent'> & RecomputeOthersSource
export type DownloadComponent = BaseComponent<'downloadcomponent'>
export type FormComponent = BaseComponent<'formcomponent'> & RecomputeOthersSource
export type FormButtonComponent = BaseComponent<'formbuttoncomponent'> & RecomputeOthersSource
@@ -164,6 +166,7 @@ export type TypedComponent =
| Schemaformcomponent
| SelectTabComponent
| ConditionalWrapperComponent
| DownloadComponent
export type AppComponent = BaseAppComponent & TypedComponent
@@ -509,6 +512,72 @@ export const components = {
}
}
},
downloadcomponent: {
name: 'Download Button',
icon: Download,
dims: '1:1-2:1' as AppComponentDimensions,
customCss: {
button: { style: '', class: '' }
},
initialData: {
...defaultAlignement,
configuration: {
source: {
type: 'static',
value: '',
fieldType: 'text',
fileUpload: {
accept: '*',
convertTo: 'base64'
},
placeholder: 'Enter URL or upload file'
},
filename: {
type: 'static',
fieldType: 'text',
value: 'windmill.file'
},
label: {
type: 'static',
fieldType: 'text',
value: 'Press me'
},
color: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
selectOptions: selectOptions.buttonColorOptions,
value: 'blue'
},
size: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
selectOptions: selectOptions.buttonSizeOptions,
value: 'xs'
},
fillContainer: {
fieldType: 'boolean',
type: 'static',
onlyStatic: true,
value: false
},
beforeIcon: {
type: 'static',
value: undefined,
fieldType: 'icon-select',
onlyStatic: true
},
afterIcon: {
type: 'static',
value: undefined,
fieldType: 'icon-select',
onlyStatic: true
}
}
}
},
formcomponent: {
name: 'Submit form',
icon: FormInput,
@@ -1746,7 +1815,8 @@ Hello \${ctx.username}
fileUpload: {
accept: 'application/pdf',
convertTo: 'base64'
}
},
placeholder: 'Enter URL or upload file'
},
zoom: {
fieldType: 'number',
@@ -21,7 +21,7 @@ const layout: ComponentSet = {
const buttons: ComponentSet = {
title: 'Buttons',
components: ['buttoncomponent', 'formcomponent', 'formbuttoncomponent']
components: ['buttoncomponent', 'formcomponent', 'formbuttoncomponent', 'downloadcomponent']
} as const
const inputs: ComponentSet = {
@@ -612,6 +612,9 @@ export const quickStyleProperties: Record<
buttoncomponent: {
button: buttonDefaultProps
},
downloadcomponent: {
button: buttonDefaultProps
},
drawercomponent: {
container: containerDefaultProps
},
+11
View File
@@ -309,3 +309,14 @@ export function tailwindVerticalAlignment(alignment?: VerticalAlignment) {
}
return classes[alignment]
}
export function transformBareBase64IfNecessary(source: string | undefined) {
if (!source) {
return source
}
if (source.startsWith('data:') || !source.includes(',')) {
return source
} else {
return `data:application/octet-stream;base64,${source}`
}
}
@@ -28,6 +28,7 @@
export let loading = false
export let title: string | undefined = undefined
export let style: string = ''
export let download: string | undefined = undefined
type MenuItem = {
label: string
@@ -111,6 +112,9 @@
event.stopPropagation()
dispatch('click', event)
if (href) {
if (href.startsWith('data')) {
return
}
if (href.startsWith('http') || target == '_blank') {
window.open(href, target)
} else {
@@ -148,6 +152,7 @@
on:click={onClick}
on:focus
on:blur
{download}
class={twMerge(buttonClass, disabled ? '!bg-gray-300 !text-gray-600 !cursor-not-allowed' : '')}
{...buttonProps}
disabled={disabled || loading}