mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
feat(frontend): App carousel (#1956)
* feat(frontend): app carousel * carousel wip * feat(frontend): add carousel component * feat(frontend): add slide handler * feat(frontend): Fix styling issues * feat(frontend): Update subgrid focus on page change * feat(frontend): fix subgrid issues + add carousel list * feat(frontend): fix carousel width issues * feat(frontend): fix build * feat(frontend): fix build * feat(frontend): revert changes * feat(frontend): add missing tooltips * feat(frontend): remove autoplay * feat(frontend): add missing tooltip + add select for timings * feat(frontend): simplify carousel * feat(frontend): fix render when data changes --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Generated
+67
@@ -33,6 +33,7 @@
|
||||
"openai": "^4.0.0-beta.4",
|
||||
"quill": "^1.3.7",
|
||||
"svelte-autosize": "^1.0.1",
|
||||
"svelte-carousel": "^1.0.25",
|
||||
"svelte-chartjs": "^3.1.0",
|
||||
"svelte-dnd-action": "^0.9.22",
|
||||
"svelte-exmarkdown": "^2.1.0",
|
||||
@@ -3497,6 +3498,16 @@
|
||||
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/easy-reactive": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/easy-reactive/-/easy-reactive-1.0.4.tgz",
|
||||
"integrity": "sha512-dFVouegPctMf6XT5si+sRByBWUQE30n99TW0Y45Y7mccmNsbSl4uXA2sBGI8zCDZqXjhb8ouA3wVVhSSGpXk5Q==",
|
||||
"dependencies": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.348",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.348.tgz",
|
||||
@@ -5136,11 +5147,26 @@
|
||||
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||
},
|
||||
"node_modules/lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
@@ -8656,6 +8682,14 @@
|
||||
"colord": "^2.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-carousel": {
|
||||
"version": "1.0.25",
|
||||
"resolved": "https://registry.npmjs.org/svelte-carousel/-/svelte-carousel-1.0.25.tgz",
|
||||
"integrity": "sha512-vR/AbGYwlh3fgWaB5PJg+iqZFvoVCJq4JWdmlXvPtdG9TLlKJWBcoiKlMZt3MnFF7AqNW46tOH/O+uVMWQzyWQ==",
|
||||
"dependencies": {
|
||||
"easy-reactive": "1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-chartjs": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz",
|
||||
@@ -12305,6 +12339,16 @@
|
||||
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
|
||||
"dev": true
|
||||
},
|
||||
"easy-reactive": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/easy-reactive/-/easy-reactive-1.0.4.tgz",
|
||||
"integrity": "sha512-dFVouegPctMf6XT5si+sRByBWUQE30n99TW0Y45Y7mccmNsbSl4uXA2sBGI8zCDZqXjhb8ouA3wVVhSSGpXk5Q==",
|
||||
"requires": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.348",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.348.tgz",
|
||||
@@ -13532,11 +13576,26 @@
|
||||
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||
},
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||
},
|
||||
"lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
@@ -15964,6 +16023,14 @@
|
||||
"colord": "^2.9.3"
|
||||
}
|
||||
},
|
||||
"svelte-carousel": {
|
||||
"version": "1.0.25",
|
||||
"resolved": "https://registry.npmjs.org/svelte-carousel/-/svelte-carousel-1.0.25.tgz",
|
||||
"integrity": "sha512-vR/AbGYwlh3fgWaB5PJg+iqZFvoVCJq4JWdmlXvPtdG9TLlKJWBcoiKlMZt3MnFF7AqNW46tOH/O+uVMWQzyWQ==",
|
||||
"requires": {
|
||||
"easy-reactive": "1.0.4"
|
||||
}
|
||||
},
|
||||
"svelte-chartjs": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.2.tgz",
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
"openai": "^4.0.0-beta.4",
|
||||
"quill": "^1.3.7",
|
||||
"svelte-autosize": "^1.0.1",
|
||||
"svelte-carousel": "^1.0.25",
|
||||
"svelte-chartjs": "^3.1.0",
|
||||
"svelte-dnd-action": "^0.9.22",
|
||||
"svelte-exmarkdown": "^2.1.0",
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { initConfig, initOutput } from '../../editor/appUtils'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import ListWrapper from '../layout/ListWrapper.svelte'
|
||||
import Carousel from 'svelte-carousel'
|
||||
import { ArrowLeftCircle, ArrowRightCircle } from 'lucide-svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
export let configuration: RichConfigurations
|
||||
export let customCss: ComponentCustomCSS<'containercomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let initializing: boolean | undefined
|
||||
export let componentContainerHeight: number
|
||||
|
||||
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
loading: false,
|
||||
inputs: {}
|
||||
})
|
||||
|
||||
const resolvedConfig = initConfig(
|
||||
components['carousellistcomponent'].initialData.configuration,
|
||||
configuration
|
||||
)
|
||||
|
||||
function onFocus() {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
$: css = concatCustomCss($app.css?.containercomponent, customCss)
|
||||
let result: any[] | undefined = undefined
|
||||
|
||||
let inputs = {}
|
||||
let carousel: Carousel
|
||||
|
||||
$: $selectedComponent?.includes(id) &&
|
||||
$focusedGrid === undefined &&
|
||||
($focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
})
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['carousellistcomponent'].initialData.configuration) as key (key)}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
{key}
|
||||
bind:resolvedConfig={resolvedConfig[key]}
|
||||
configuration={configuration[key]}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<RunnableWrapper
|
||||
render={true}
|
||||
{outputs}
|
||||
autoRefresh
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
>
|
||||
<div class="w-full flex flex-wrap overflow-auto divide-y max-h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
{#if Array.isArray(result) && result.length > 0}
|
||||
{#key result}
|
||||
<Carousel
|
||||
particlesToShow={1}
|
||||
particlesToScroll={1}
|
||||
autoplay={false}
|
||||
autoplayProgressVisible={false}
|
||||
timingFunction={resolvedConfig.timingFunction}
|
||||
dots={true}
|
||||
arrows={true}
|
||||
swiping={false}
|
||||
bind:this={carousel}
|
||||
let:currentPageIndex
|
||||
on:pageChange={(event) => {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: event.detail
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div slot="prev" class="h-full flex justify-center flex-col p-2">
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
on:click={() => {
|
||||
const pagesCount = result?.length ?? 0
|
||||
|
||||
if (currentPageIndex > 0) {
|
||||
carousel.goTo(currentPageIndex - 1)
|
||||
} else {
|
||||
carousel.goTo(pagesCount - 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowLeftCircle size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="next" class="h-full flex justify-center flex-col p-2">
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
on:click={() => {
|
||||
const pagesCount = result?.length ?? 0
|
||||
if (currentPageIndex < pagesCount - 1) {
|
||||
carousel.goTo(currentPageIndex + 1)
|
||||
} else {
|
||||
carousel.goTo(0)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowRightCircle size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#each result ?? [] as value, index}
|
||||
<div class="overflow-auto w-full">
|
||||
<ListWrapper
|
||||
on:inputsChange={() => {
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
bind:inputs
|
||||
{value}
|
||||
{index}
|
||||
>
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render}
|
||||
class={css?.container?.class}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight - 40}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
</ListWrapper>
|
||||
</div>
|
||||
{/each}
|
||||
</Carousel>
|
||||
{/key}
|
||||
{:else}
|
||||
<ListWrapper disabled value={undefined} index={0}>
|
||||
<SubGridEditor visible={false} {id} subGridId={`${id}-0`} />
|
||||
</ListWrapper>
|
||||
{#if !Array.isArray(result)}
|
||||
<div class="text-center text-tertiary">Input data is not an array</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</RunnableWrapper>
|
||||
@@ -54,6 +54,7 @@
|
||||
import AppList from '../../components/layout/AppList.svelte'
|
||||
import AppJobIdLogComponent from '../../components/display/AppJobIdLogComponent.svelte'
|
||||
import AppJobIdFlowStatus from '../../components/display/AppJobIdFlowStatus.svelte'
|
||||
import AppCarouselList from '../../components/display/AppCarouselList.svelte'
|
||||
|
||||
export let component: AppComponent
|
||||
export let selected: boolean
|
||||
@@ -600,6 +601,15 @@
|
||||
componentInput={component.componentInput}
|
||||
{render}
|
||||
/>
|
||||
{:else if component.type === 'carousellistcomponent'}
|
||||
<AppCarouselList
|
||||
id={component.id}
|
||||
configuration={component.configuration}
|
||||
componentInput={component.componentInput}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -137,6 +137,8 @@ export type Schemaformcomponent = BaseComponent<'schemaformcomponent'>
|
||||
export type SelectTabComponent = BaseComponent<'selecttabcomponent'>
|
||||
export type SelectStepComponent = BaseComponent<'selectstepcomponent'>
|
||||
|
||||
export type CarouselListComponent = BaseComponent<'carousellistcomponent'>
|
||||
|
||||
export type TypedComponent =
|
||||
| DisplayComponent
|
||||
| LogComponent
|
||||
@@ -192,6 +194,7 @@ export type TypedComponent =
|
||||
| SelectStepComponent
|
||||
| DownloadComponent
|
||||
| ChartJsComponent
|
||||
| CarouselListComponent
|
||||
|
||||
export type AppComponent = BaseAppComponent & TypedComponent
|
||||
|
||||
@@ -281,6 +284,7 @@ export const selectOptions = {
|
||||
'radar',
|
||||
'scatter'
|
||||
] as ChartType[],
|
||||
animationTimingFunctionOptions: ['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out'],
|
||||
prose: ['sm', 'Default', 'lg', 'xl', '2xl']
|
||||
}
|
||||
const labels = {
|
||||
@@ -1920,6 +1924,34 @@ This is a paragraph.
|
||||
tabs: ['First', 'Second'] as string[]
|
||||
}
|
||||
},
|
||||
carousellistcomponent: {
|
||||
name: 'Carousel List',
|
||||
icon: ListIcon,
|
||||
documentationLink: `${documentationBaseUrl}#list`,
|
||||
dims: '3:8-12:8' as AppComponentDimensions,
|
||||
customCss: {
|
||||
container: { class: '', style: '' }
|
||||
},
|
||||
initialData: {
|
||||
configuration: {
|
||||
timingFunction: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
onlyStatic: true,
|
||||
selectOptions: selectOptions.animationTimingFunctionOptions,
|
||||
value: 'linear',
|
||||
tooltip: 'See https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function'
|
||||
}
|
||||
},
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'object',
|
||||
value: [{ foo: 1 }, { foo: 2 }, { foo: 3 }] as object[]
|
||||
},
|
||||
numberOfSubgrids: 1
|
||||
}
|
||||
},
|
||||
iconcomponent: {
|
||||
name: 'Icon',
|
||||
icon: Smile,
|
||||
|
||||
@@ -17,7 +17,8 @@ const layout: ComponentSet = {
|
||||
'verticalsplitpanescomponent',
|
||||
'horizontalsplitpanescomponent',
|
||||
'modalcomponent',
|
||||
'steppercomponent'
|
||||
'steppercomponent',
|
||||
'carousellistcomponent'
|
||||
]
|
||||
} as const
|
||||
|
||||
|
||||
@@ -643,6 +643,9 @@ export const quickStyleProperties: Record<
|
||||
header: [...containerDefaultProps, typographyGrouping],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
carousellistcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
flowstatuscomponent: {
|
||||
header: [...containerDefaultProps, typographyGrouping],
|
||||
container: containerDefaultProps
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
export let canDisableTabs: boolean = false
|
||||
|
||||
// Migration code
|
||||
$: if (tabs.length !== disabledTabs?.length && canDisableTabs) {
|
||||
$: if (tabs?.length !== disabledTabs?.length && canDisableTabs) {
|
||||
disabledTabs = Array(tabs.length).fill({
|
||||
type: 'static',
|
||||
value: false,
|
||||
|
||||
@@ -247,8 +247,6 @@ export function concatCustomCss<T extends Record<string, ComponentCssProperty>>(
|
||||
const compStyle = v?.style?.trim() || ''
|
||||
const compEnding = compStyle?.endsWith(';') || !compStyle ? ' ' : ';'
|
||||
|
||||
console.log(key, v)
|
||||
|
||||
return [
|
||||
key,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user