mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
feat: app builder accordion list and accordion tab component (#5132)
* feat: tabs component accordion * accordion tabs * accordionlistcomponent * types * Update AppTabs.svelte --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { initOutput } from '../../editor/appUtils'
|
||||
import SubGridEditor from '../../editor/SubGridEditor.svelte'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
|
||||
import { initCss } from '../../utils'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
|
||||
import ListWrapper from '../layout/ListWrapper.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
export let customCss: ComponentCustomCSS<'accordionlistcomponent'> | undefined = undefined
|
||||
export let render: boolean
|
||||
export let initializing: boolean | undefined
|
||||
export let componentContainerHeight: number
|
||||
|
||||
type AccordionListValue = { header: string; [key: string]: any };
|
||||
|
||||
type InternalAccordionListInput = AppInput & {
|
||||
value: AccordionListValue[];
|
||||
};
|
||||
const accordionInput = componentInput as InternalAccordionListInput
|
||||
|
||||
const { app, focusedGrid, selectedComponent, worldStore, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
let activeIndex: number = 0
|
||||
|
||||
const outputs = initOutput($worldStore, id, {
|
||||
result: undefined,
|
||||
activeIndex: 0,
|
||||
loading: false,
|
||||
inputs: {}
|
||||
})
|
||||
|
||||
function onFocus() {
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
}
|
||||
}
|
||||
|
||||
let css = initCss($app.css?.accordionlistcomponent, customCss)
|
||||
let result: any[] | undefined = undefined
|
||||
|
||||
let inputs = {}
|
||||
|
||||
$: $selectedComponent?.includes(id) &&
|
||||
$focusedGrid === undefined &&
|
||||
($focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
})
|
||||
|
||||
function toggleAccordion(index: number) {
|
||||
activeIndex = activeIndex === index ? -1 : index
|
||||
outputs.activeIndex.set(activeIndex)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#each Object.keys(css ?? {}) as key (key)}
|
||||
<ResolveStyle
|
||||
{id}
|
||||
{customCss}
|
||||
{key}
|
||||
bind:css={css[key]}
|
||||
componentStyle={$app.css?.accordionlistcomponent}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<InitializeComponent {id} />
|
||||
|
||||
<RunnableWrapper
|
||||
render={true}
|
||||
{outputs}
|
||||
autoRefresh
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
>
|
||||
<div class="w-full flex flex-col overflow-auto max-h-full">
|
||||
{#if $app.subgrids?.[`${id}-0`]}
|
||||
{#if Array.isArray(result) && result.length > 0}
|
||||
{#each result ?? [] as value, index}
|
||||
<div class="border-b">
|
||||
<button
|
||||
on:pointerdown|stopPropagation
|
||||
on:click={() => toggleAccordion(index)}
|
||||
class={twMerge(
|
||||
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
|
||||
'wm-tabs-alltabs',
|
||||
activeIndex === index
|
||||
? twMerge('bg-surface text-primary ', 'wm-tabs-selectedTab')
|
||||
: 'text-secondary'
|
||||
)}
|
||||
>
|
||||
<span class="mr-2 w-8 font-mono">{activeIndex === index ? '-' : '+'}</span>
|
||||
{accordionInput?.value[index].header || `Header ${index}`}
|
||||
</button>
|
||||
{#if activeIndex === index}
|
||||
<div class="p-2 overflow-auto w-full">
|
||||
<ListWrapper
|
||||
onSet={(id, value) => {
|
||||
if (!inputs[id]) {
|
||||
inputs[id] = { [index]: value }
|
||||
} else {
|
||||
inputs[id] = { ...inputs[id], [index]: value }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
onRemove={(id) => {
|
||||
if (inputs?.[id] == undefined) {
|
||||
return
|
||||
}
|
||||
if (index == 0) {
|
||||
delete inputs[id]
|
||||
inputs = { ...inputs }
|
||||
} else {
|
||||
delete inputs[id][index]
|
||||
inputs[id] = { ...inputs[id] }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
{value}
|
||||
{index}
|
||||
>
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render}
|
||||
class={twMerge(css?.container?.class, 'wm-accordion')}
|
||||
style={css?.container?.style}
|
||||
subGridId={`${id}-0`}
|
||||
containerHeight={componentContainerHeight -
|
||||
(30 * accordionInput?.value.length + 40)}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
}
|
||||
onFocus()
|
||||
}}
|
||||
/>
|
||||
</ListWrapper>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{: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>
|
||||
@@ -48,6 +48,8 @@
|
||||
selectedTabIndex: 0
|
||||
})
|
||||
|
||||
const titleBarHeight = 40; // Example height of a single accordion title bar in pixels
|
||||
|
||||
function handleTabSelection() {
|
||||
selectedIndex = tabs?.indexOf(selected)
|
||||
outputs?.selectedTabIndex.set(selectedIndex)
|
||||
@@ -170,28 +172,72 @@
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && i === selectedIndex}
|
||||
subGridId={`${id}-${i}`}
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
|
||||
? componentContainerHeight - tabHeight
|
||||
: componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if resolvedConfig.tabsKind == 'accordion'}
|
||||
<div class="flex flex-col w-full">
|
||||
{#each tabs ?? [] as res, index}
|
||||
<div class="border-b">
|
||||
<button
|
||||
on:pointerdown|stopPropagation
|
||||
on:click={() => (selected = res)}
|
||||
class={twMerge(
|
||||
'w-full text-left bg-surface !truncate text-sm hover:text-primary px-1 py-2',
|
||||
css?.allTabs?.class,
|
||||
'wm-tabs-alltabs',
|
||||
selected == res
|
||||
? twMerge(
|
||||
'bg-surface text-primary ',
|
||||
css?.selectedTab?.class,
|
||||
'wm-tabs-selectedTab'
|
||||
)
|
||||
: 'text-secondary'
|
||||
)}
|
||||
>
|
||||
<span class="mr-2 w-8 font-mono">{selected == res ? '-' : '+'}</span>
|
||||
{res}
|
||||
</button>
|
||||
{#if selected == res}
|
||||
<div class="p-2 border-t">
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && index === selectedIndex}
|
||||
subGridId={`${id}-${index}`}
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={componentContainerHeight - (titleBarHeight * tabs.length + 40)}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
{#if $app.subgrids}
|
||||
{#each tabs ?? [] as _res, i}
|
||||
<SubGridEditor
|
||||
{id}
|
||||
visible={render && i === selectedIndex}
|
||||
subGridId={`${id}-${i}`}
|
||||
class={twMerge(css?.container?.class, 'wm-tabs-container')}
|
||||
style={css?.container?.style}
|
||||
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
|
||||
? componentContainerHeight - tabHeight
|
||||
: componentContainerHeight}
|
||||
on:focus={() => {
|
||||
if (!$connectingInput.opened) {
|
||||
$selectedComponent = [id]
|
||||
handleTabSelection()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import AppJobIdLogComponent from '../../components/display/AppJobIdLogComponent.svelte'
|
||||
import AppJobIdFlowStatus from '../../components/display/AppJobIdFlowStatus.svelte'
|
||||
import AppCarouselList from '../../components/display/AppCarouselList.svelte'
|
||||
import AppAccordionList from '../../components/display/AppAccordionList.svelte'
|
||||
import AppAggridTableEe from '../../components/display/table/AppAggridTableEe.svelte'
|
||||
import AppCustomComponent from '../../components/display/AppCustomComponent.svelte'
|
||||
import AppStatCard from '../../components/display/AppStatCard.svelte'
|
||||
@@ -871,6 +872,15 @@
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'accordionlistcomponent'}
|
||||
<AppAccordionList
|
||||
id={component.id}
|
||||
componentInput={component.componentInput}
|
||||
customCss={component.customCss}
|
||||
{componentContainerHeight}
|
||||
{render}
|
||||
bind:initializing
|
||||
/>
|
||||
{:else if component.type === 'statcomponent'}
|
||||
<AppStatCard
|
||||
id={component.id}
|
||||
|
||||
@@ -51,7 +51,9 @@ import {
|
||||
CalendarClock,
|
||||
AppWindow,
|
||||
PanelTop,
|
||||
RefreshCw
|
||||
RefreshCw,
|
||||
ListCollapse,
|
||||
GalleryThumbnails
|
||||
} from 'lucide-svelte'
|
||||
import type {
|
||||
Aligned,
|
||||
@@ -244,6 +246,7 @@ export type SelectTabComponent = BaseComponent<'selecttabcomponent'>
|
||||
export type SelectStepComponent = BaseComponent<'selectstepcomponent'>
|
||||
|
||||
export type CarouselListComponent = BaseComponent<'carousellistcomponent'>
|
||||
export type AccordionListComponent = BaseComponent<'accordionlistcomponent'>
|
||||
export type StatisticCardComponent = BaseComponent<'statcomponent'>
|
||||
export type MenuComponent = BaseComponent<'menucomponent'> & {
|
||||
menuItems: (BaseAppComponent & ButtonComponent & GridItem)[]
|
||||
@@ -351,6 +354,7 @@ export type TypedComponent =
|
||||
| DownloadComponent
|
||||
| ChartJsComponent
|
||||
| CarouselListComponent
|
||||
| AccordionListComponent
|
||||
| PlotlyComponentV2
|
||||
| ChartJsComponentV2
|
||||
| StatisticCardComponent
|
||||
@@ -446,7 +450,7 @@ const buttonColorOptions = [...BUTTON_COLORS]
|
||||
|
||||
export const selectOptions = {
|
||||
buttonColorOptions,
|
||||
tabsKindOptions: ['tabs', 'sidebar', 'invisibleOnView'],
|
||||
tabsKindOptions: ['tabs', 'sidebar', 'accordion', 'invisibleOnView'],
|
||||
buttonSizeOptions: ['xs', 'sm', 'md', 'lg', 'xl'],
|
||||
tableSearchOptions: ['By Component', 'By Runnable', 'Disabled'],
|
||||
chartThemeOptions: ['theme1', 'theme2', 'theme3'],
|
||||
@@ -2852,7 +2856,7 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
},
|
||||
carousellistcomponent: {
|
||||
name: 'Carousel List',
|
||||
icon: ListIcon,
|
||||
icon: GalleryThumbnails,
|
||||
documentationLink: `${documentationBaseUrl}/carousel`,
|
||||
dims: '3:8-12:8' as AppComponentDimensions,
|
||||
customCss: {
|
||||
@@ -2878,6 +2882,25 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
numberOfSubgrids: 1
|
||||
}
|
||||
},
|
||||
accordionlistcomponent: {
|
||||
name: 'Accordion List',
|
||||
icon: ListCollapse,
|
||||
documentationLink: `${documentationBaseUrl}/accordion`,
|
||||
dims: '3:8-12:8' as AppComponentDimensions,
|
||||
customCss: {
|
||||
container: { class: '', style: '' }
|
||||
},
|
||||
initialData: {
|
||||
configuration: {},
|
||||
componentInput: {
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'object',
|
||||
value: [{ header: 'First', foo: 1 }, { header: 'Second', foo: 2 }, { header: 'Third', foo: 3 }] as object[]
|
||||
},
|
||||
numberOfSubgrids: 1
|
||||
}
|
||||
},
|
||||
iconcomponent: {
|
||||
name: 'Icon',
|
||||
icon: Smile,
|
||||
@@ -4152,6 +4175,17 @@ export const presetComponents = {
|
||||
},
|
||||
type: 'sidebartabscomponent'
|
||||
},
|
||||
accordiontabcomponent: {
|
||||
name: 'Accordion Tabs',
|
||||
icon: ListCollapse,
|
||||
targetComponent: 'tabscomponent' as const,
|
||||
configuration: {
|
||||
tabsKind: {
|
||||
value: 'accordion'
|
||||
}
|
||||
},
|
||||
type: 'accordiontabcomponent'
|
||||
},
|
||||
invisibletabscomponent: {
|
||||
name: 'Invisible Tabs',
|
||||
icon: PanelTopInactive,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ComponentSet } from '../../types'
|
||||
const tabs: ComponentSet = {
|
||||
title: 'Tabs',
|
||||
components: ['tabscomponent', 'conditionalwrapper'],
|
||||
presets: ['sidebartabscomponent', 'invisibletabscomponent']
|
||||
presets: ['sidebartabscomponent', 'accordiontabcomponent', 'invisibletabscomponent']
|
||||
} as const
|
||||
|
||||
const layout: ComponentSet = {
|
||||
@@ -19,6 +19,7 @@ const layout: ComponentSet = {
|
||||
'modalcomponent',
|
||||
'steppercomponent',
|
||||
'carousellistcomponent',
|
||||
'accordionlistcomponent',
|
||||
'decisiontreecomponent',
|
||||
'navbarcomponent',
|
||||
'recomputeallcomponent'
|
||||
|
||||
@@ -299,6 +299,13 @@ export const customisationByComponent: Customisation[] = [
|
||||
],
|
||||
variables: []
|
||||
},
|
||||
{
|
||||
components: ['accordionlistcomponent'],
|
||||
selectors: [
|
||||
{ selector: '.wm-accordion', comment: 'Accordion component', customCssKey: 'container' }
|
||||
],
|
||||
variables: []
|
||||
},
|
||||
{
|
||||
components: ['formcomponent'],
|
||||
selectors: [
|
||||
|
||||
@@ -654,6 +654,9 @@ export const quickStyleProperties: Record<
|
||||
header: [...containerDefaultProps, typographyGrouping],
|
||||
container: containerDefaultProps
|
||||
},
|
||||
accordionlistcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
carousellistcomponent: {
|
||||
container: containerDefaultProps
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user