mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 16:02:23 +00:00
feat(frontend): add download button
This commit is contained in:
@@ -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
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user