feat(frontend): Add image app component (#1213)

* fix(frontend): hard type app component config

* feat(frontend): Add image app component

* feat(frontend): Add upload app input type
This commit is contained in:
Ádám Kovács
2023-02-21 19:14:55 +01:00
committed by GitHub
parent 61e6e1a4c5
commit a4b773af29
13 changed files with 257 additions and 72 deletions
+7
View File
@@ -27,6 +27,13 @@ export type Schema = {
export type Meta = { ownerKind: OwnerKind; owner: string; name: string }
type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>
/** An inclusive range of integer numbers */
export type IntRange<F extends number, T extends number> = F | Exclude<Enumerate<T>, Enumerate<F>> | T
export function pathToMeta(path: string): Meta {
const splitted = path.split('/')
let ownerKind: OwnerKind
@@ -0,0 +1,34 @@
<script lang="ts">
import type { staticValues } from '../../editor/componentsPanel/componentStaticValues'
import type { AppInput } from '../../inputType'
import InputValue from '../helpers/InputValue.svelte'
type FitOption = typeof staticValues['objectFitOptions'][number]
export let id: string
export let configuration: Record<string, AppInput>
export const staticOutputs: string[] = ['loading']
const fit: Record<FitOption, string> = {
cover: 'object-cover',
contain: 'object-contain',
fill: 'object-fill',
}
let source: string | undefined = undefined
let imageFit: FitOption | undefined = undefined
let altText: string | undefined = undefined
let customStyles: string | undefined = undefined
</script>
<InputValue {id} input={configuration.source} bind:value={source} />
<InputValue {id} input={configuration.imageFit} bind:value={imageFit} />
<InputValue {id} input={configuration.altText} bind:value={altText} />
<InputValue {id} input={configuration.customStyles} bind:value={customStyles} />
<img
src={source}
alt={altText}
style={customStyles}
class="w-full h-full {fit[imageFit || 'cover']}"
>
@@ -1,7 +1,7 @@
<script lang="ts">
import { isCodeInjection } from '$lib/components/flows/utils'
import { getContext } from 'svelte'
import type { AppInput, EvalAppInput } from '../../inputType'
import type { AppInput, EvalAppInput, UploadAppInput } from '../../inputType'
import type { AppEditorContext } from '../../types'
import { accessPropertyByPath } from '../../utils'
@@ -29,6 +29,8 @@
setTimeout(() => (value = getValue(input)), 0)
} else if (input.type == 'eval') {
setTimeout(() => ((value = evalExpr(input as EvalAppInput)), 0))
} else if (input.type == 'upload') {
setTimeout(() => ((value = (input as UploadAppInput).value), 0))
} else {
setTimeout(() => (value = undefined), 0)
}
@@ -21,7 +21,8 @@
DollarSign,
SeparatorHorizontal,
SeparatorVertical,
Paperclip
Paperclip,
Image
} from 'lucide-svelte'
import type { Size } from 'svelte-grid'
import { twMerge } from 'tailwind-merge'
@@ -107,19 +108,25 @@
| HorizontalDividerComponent
| VerticalDividerComponent
| FileInputComponent
| ImageComponent
)
// Dimensions key formula: <mobile width>:<mobile height>-<desktop width>:<desktop height>
export const components: Record<
AppComponent['type'],
{
export type AppComponentDimensions =
`${IntRange<1, typeof NARROW_GRID_COLUMNS>}:${number}-${IntRange<1, typeof WIDE_GRID_COLUMNS>}:${number}`
export type AppComponentConfig = {
name: string
icon: any
dims: `${number}:${number}-${number}:${number}`
/**
* Dimensions key formula:
* [**mobile width**]:[**mobile height**]-[**desktop width**]:[**desktop height**]
*/
dims: AppComponentDimensions
data: AppComponent
cssIds?: string[]
}
> = {
export const components: Record<AppComponent['type'], AppComponentConfig> = {
displaycomponent: {
name: 'Rich Result',
icon: Monitor,
@@ -834,7 +841,7 @@
slidercomponent: {
name: 'Slider',
icon: SlidersHorizontal,
dims: '4:1-4:1',
dims: '3:1-4:1',
data: {
softWrap: true,
verticalAlignment: 'center',
@@ -874,7 +881,7 @@
rangecomponent: {
name: 'Range',
icon: SlidersHorizontal,
dims: '4:2-4:2',
dims: '3:2-4:2',
data: {
softWrap: true,
verticalAlignment: 'center',
@@ -1099,6 +1106,49 @@
card: false
}
},
imagecomponent: {
name: 'Image',
icon: Image,
dims: '3:4-5:4',
data: {
id: '',
type: 'imagecomponent',
componentInput: undefined,
configuration: {
source: {
type: 'static',
value: '/logo.svg',
fieldType: 'text',
fileUpload: {
accept: 'image/*',
base64: true
}
},
imageFit: {
fieldType: 'select',
type: 'static',
onlyStatic: true,
optionValuesKey: 'objectFitOptions',
value: 'contain'
},
altText: {
type: 'static',
value: '',
fieldType: 'text',
onlyStatic: true,
tooltip: 'This text will appear if the image can\'t be loaded for any reason'
},
customStyles: {
type: 'static',
value: '',
fieldType: 'textarea',
onlyStatic: true,
},
},
customCss: {},
card: false
}
},
}
const inputs: ComponentSet = {
@@ -1127,6 +1177,7 @@
components: [
'textcomponent',
'iconcomponent',
'imagecomponent',
'htmlcomponent',
'tablecomponent',
'barchartcomponent',
@@ -1509,6 +1560,9 @@
import AppCurrencyInput from '../components/numberInputs/AppCurrencyInput.svelte'
import AppDivider from '../components/AppDivider.svelte'
import AppFileInput from '../components/otherInputs/AppFileInput.svelte'
import type { IntRange } from '../../../common'
import type { NARROW_GRID_COLUMNS, WIDE_GRID_COLUMNS } from '../gridUtils'
import AppImage from '../components/dataDisplay/AppImage.svelte'
export let component: AppComponent
export let selected: boolean
@@ -1674,6 +1728,8 @@
<AppIcon {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{:else if component.type === 'fileinputcomponent'}
<AppFileInput {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{:else if component.type === 'imagecomponent'}
<AppImage {...component} bind:staticOutputs={$staticOutputs[component.id]} />
{/if}
</div>
</div>
@@ -9,5 +9,6 @@ export const staticValues = {
chartThemeOptions: ['theme1', 'theme2', 'theme3'],
textStyleOptions: ['Title', 'Subtitle', 'Body', 'Label', 'Caption'],
currencyOptions: ['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY', 'CNY', 'INR', 'BRL'],
localeOptions: ['en-US', 'en-GB', 'en-IE', 'de-DE', 'fr-FR', 'br-FR', 'ja-JP', 'pt-TL', 'fr-CA', 'en-CA']
localeOptions: ['en-US', 'en-GB', 'en-IE', 'de-DE', 'fr-FR', 'br-FR', 'ja-JP', 'pt-TL', 'fr-CA', 'en-CA'],
objectFitOptions: ['contain', 'cover', 'fill']
} as const
@@ -14,43 +14,45 @@
export let component: AppComponent
</script>
<PanelSection title="Alignment">
<div class="flex flex-wrap gap-2">
{#if component.horizontalAlignment}
<div class="flex flex-col gap-0.5">
<div class="text-xs font-semibold">Horizontal</div>
<div>
<ToggleButtonGroup bind:selected={component.horizontalAlignment}>
<ToggleButton position="left" value="left" size="xs">
<AlignStartVertical size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterVertical size={16} />
</ToggleButton>
<ToggleButton position="right" value="right" size="xs">
<AlignEndVertical size={16} />
</ToggleButton>
</ToggleButtonGroup>
{#if component.horizontalAlignment || component.verticalAlignment}
<PanelSection title="Alignment">
<div class="flex flex-wrap gap-2">
{#if component.horizontalAlignment}
<div class="flex flex-col gap-0.5">
<div class="text-xs font-semibold">Horizontal</div>
<div>
<ToggleButtonGroup bind:selected={component.horizontalAlignment}>
<ToggleButton position="left" value="left" size="xs">
<AlignStartVertical size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterVertical size={16} />
</ToggleButton>
<ToggleButton position="right" value="right" size="xs">
<AlignEndVertical size={16} />
</ToggleButton>
</ToggleButtonGroup>
</div>
</div>
</div>
{/if}
{#if component.type !== 'formcomponent' && component.verticalAlignment}
<div class="flex flex-col gap-0.5">
<div class="text-xs font-semibold">Vertical</div>
<div>
<ToggleButtonGroup bind:selected={component.verticalAlignment}>
<ToggleButton position="left" value="top" size="xs">
<AlignStartHorizontal size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterHorizontal size={16} />
</ToggleButton>
<ToggleButton position="right" value="bottom" size="xs">
<AlignEndHorizontal size={16} />
</ToggleButton>
</ToggleButtonGroup>
{/if}
{#if component.type !== 'formcomponent' && component.verticalAlignment}
<div class="flex flex-col gap-0.5">
<div class="text-xs font-semibold">Vertical</div>
<div>
<ToggleButtonGroup bind:selected={component.verticalAlignment}>
<ToggleButton position="left" value="top" size="xs">
<AlignStartHorizontal size={16} />
</ToggleButton>
<ToggleButton position="center" value="center" size="xs">
<AlignCenterHorizontal size={16} />
</ToggleButton>
<ToggleButton position="right" value="bottom" size="xs">
<AlignEndHorizontal size={16} />
</ToggleButton>
</ToggleButtonGroup>
</div>
</div>
</div>
{/if}
</div>
</PanelSection>
{/if}
</div>
</PanelSection>
{/if}
@@ -4,12 +4,14 @@
EvalAppInput,
RowAppInput,
StaticAppInput,
UploadAppInput,
UserAppInput
} from '../../inputType'
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
import EvalInputEditor from './inputEditor/EvalInputEditor.svelte'
import RowInputEditor from './inputEditor/RowInputEditor.svelte'
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
import UploadInputEditor from './inputEditor/UploadInputEditor.svelte'
export let id: string
export let componentInput:
@@ -18,6 +20,7 @@
| UserAppInput
| RowAppInput
| EvalAppInput
| UploadAppInput
export let hasRows: boolean = false
</script>
@@ -29,6 +32,8 @@
<StaticInputEditor bind:componentInput />
{:else if componentInput.type === 'eval'}
<EvalInputEditor {hasRows} {id} bind:componentInput />
{:else if componentInput.type === 'upload'}
<UploadInputEditor bind:componentInput />
{:else if componentInput.type === 'user'}
<span class="text-2xs italic text-gray-6f00">Field's value is set by the user</span>
{/if}
@@ -1,7 +1,7 @@
<script lang="ts">
import { Badge, ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import { addWhitespaceBeforeCapitals, capitalize } from '$lib/utils'
import { faArrowRight, faPen, faTableCells, faUser } from '@fortawesome/free-solid-svg-icons'
import { faArrowRight, faPen, faUpload, faUser } from '@fortawesome/free-solid-svg-icons'
import { fieldTypeToTsType } from '../../utils'
import InputsSpecEditor from './InputsSpecEditor.svelte'
import { getContext } from 'svelte'
@@ -94,6 +94,18 @@
<svelte:fragment slot="text">User Input</svelte:fragment>
</Popover>
{/if}
{#if 'fileUpload' in inputSpecs[inputSpecKey]}
<Popover placement="bottom" notClickable disapperTimoout={0}>
<ToggleButton
position="center"
value="upload"
startIcon={{ icon: faUpload }}
size="xs"
iconOnly
/>
<svelte:fragment slot="text">Upload</svelte:fragment>
</Popover>
{/if}
<Popover placement="bottom" notClickable disapperTimoout={0}>
<ToggleButton
position="right"
@@ -0,0 +1,26 @@
<script lang="ts">
import { FileInput } from "../../../../common"
import type { UploadAppInput } from "../../../inputType"
export let componentInput: UploadAppInput | undefined
</script>
<FileInput
accept={componentInput?.fileUpload?.accept}
multiple={componentInput?.fileUpload?.multiple}
convertToBase64={componentInput?.fileUpload?.base64}
iconSize={24}
class="text-sm py-4"
on:change={({detail}) => {
if(componentInput) {
componentInput.value = componentInput?.fileUpload?.multiple
? detail
: detail?.[0]
}
}}
>
<svelte:fragment slot="selected-title">
<!-- Removing the title when there is a selected file -->
<span></span>
</svelte:fragment>
</FileInput>
@@ -7,9 +7,12 @@ const Breakpoints = {
lg: 1024
}
const WIDE_GRID_COLUMNS = 12 as const;
const NARROW_GRID_COLUMNS = 3 as const;
const columnConfiguration: ColumnConfiguration = [
[Breakpoints.lg, 12],
[Breakpoints.sm, 3]
[Breakpoints.lg, WIDE_GRID_COLUMNS],
[Breakpoints.sm, NARROW_GRID_COLUMNS]
]
const gridColumns = columnConfiguration.map((value) => value[1])
@@ -51,6 +54,8 @@ function enableDrag(component: GridItem): GridItem {
export {
gridColumns,
WIDE_GRID_COLUMNS,
NARROW_GRID_COLUMNS,
columnConfiguration,
disableDrag,
enableDrag,
+31 -11
View File
@@ -35,7 +35,11 @@ export type UserInput<U> = {
value: U | undefined
}
// Input can be uploaded with a file selector
export type UploadInput = {
type: 'upload'
value: string
}
export type EvalInput = {
type: 'eval'
@@ -88,6 +92,7 @@ type AppInputSpec<T extends InputType, U, V extends InputType = never> = (
| UserInput<U>
| RowInput
| EvalInput
| UploadInput
| ResultInput
| TemplateInput
) &
@@ -96,7 +101,28 @@ type AppInputSpec<T extends InputType, U, V extends InputType = never> = (
type InputConfiguration<T extends InputType, U, V extends InputType> = {
fieldType: T
subFieldType?: V
format?: string | undefined
format?: string | undefined,
fileUpload?: {
/** Use `*` to accept anything. */
accept: string
/**
* Controls if user is allowed to select multiple files.
* @default false
*/
multiple?: boolean
/**
* Controls if the uploaded file(s) will be returned as `Base64` strings.
* @default false
*/
base64?: boolean
}
}
type StaticOptions = {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
}
export type AppInput =
@@ -111,12 +137,7 @@ export type AppInput =
| AppInputSpec<'any', any>
| AppInputSpec<'object', Record<string | number, any>>
| AppInputSpec<'object', string>
| (AppInputSpec<'select', string> & {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
| (AppInputSpec<'select', string> & StaticOptions)
| AppInputSpec<'icon-select', string>
| AppInputSpec<'array', string[], 'text'>
| AppInputSpec<'array', string[], 'textarea'>
@@ -126,9 +147,7 @@ export type AppInput =
| AppInputSpec<'array', string[], 'time'>
| AppInputSpec<'array', string[], 'datetime'>
| AppInputSpec<'array', object[], 'object'>
| (AppInputSpec<'array', string[], 'select'> & {
optionValuesKey: keyof typeof staticValues
})
| (AppInputSpec<'array', string[], 'select'> & StaticOptions)
export type RowAppInput = Extract<AppInput, { type: 'row' }>
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
@@ -136,5 +155,6 @@ export type ConnectedAppInput = Extract<AppInput, { type: 'connected' }>
export type UserAppInput = Extract<AppInput, { type: 'user' }>
export type ResultAppInput = Extract<AppInput, { type: 'runnable' }>
export type EvalAppInput = Extract<AppInput, { type: 'eval' }>
export type UploadAppInput = Extract<AppInput, { type: 'upload' }>
export type AppInputs = Record<string, AppInput>
+15 -5
View File
@@ -10,6 +10,7 @@ import type {
EvalAppInput,
RowAppInput,
StaticAppInput,
UploadAppInput,
UserAppInput
} from './inputType'
import type { World } from './rx'
@@ -23,16 +24,25 @@ export type Aligned = {
verticalAlignment: VerticalAlignment
}
export interface GeneralAppInput {
onlyStatic?: boolean
evaluatedValue?: boolean
tooltip?: string
}
export interface BaseAppComponent extends Partial<Aligned> {
id: ComponentID
componentInput: AppInput | undefined
configuration: Record<
string,
(StaticAppInput | ConnectedAppInput | UserAppInput | RowAppInput | EvalAppInput) & {
onlyStatic?: boolean
evaluatedValue?: boolean
tooltip?: string
}
GeneralAppInput & (
StaticAppInput
| ConnectedAppInput
| UserAppInput
| RowAppInput
| EvalAppInput
| UploadAppInput
)
>
card: boolean | undefined
customCss?: Record<string, {
@@ -7,9 +7,10 @@
let c = ''
export { c as class }
export let accept = '*'
export let multiple = true
export let multiple = false
export let convertToBase64 = false
export let hideIcon = false
export let iconSize = 36
const dispatch = createEventDispatcher()
let input: HTMLInputElement
let files: File[] | undefined = undefined
@@ -72,11 +73,15 @@
duration-200 rounded-lg p-1 {c ?? ''}"
>
{#if !hideIcon && !files}
<FileUp size={36} class="mb-2" />
<FileUp size={iconSize} class="mb-2" />
{/if}
{#if files}
<div class="w-full max-h-full overflow-auto px-6">
<div class="text-center mb-2 px-2">Selected file{files.length > 1 ? 's' : ''}:</div>
<slot name="selected-title">
<div class="text-center mb-2 px-2">
Selected file{files.length > 1 ? 's' : ''}:
</div>
</slot>
<ul class="relative z-20 max-w-[250px] bg-white rounded-lg overflow-hidden mx-auto">
{#each files as {name}, i}
<li class="flex justify-between items-center font-normal text-sm
@@ -97,7 +102,7 @@
</div>
{:else}
<slot>
<span>Drag and drop files</span>
<span>Drag and drop {multiple ? 'files' : 'a file'}</span>
</slot>
{/if}
<input