mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
feat(frontend): Improve tables (#3577)
* feat(frontend): wip * feat(frontend): wip * feat(frontend): aggrid theme * feat(frontend): aggrid theme * feat(frontend): aggrid theme * feat(frontend): support old themes
This commit is contained in:
@@ -135,6 +135,11 @@
|
||||
@apply flex flex-col justify-start items-end;
|
||||
}
|
||||
.ol-control button {
|
||||
@apply w-7 h-7 center-center bg-surface border text-secondary
|
||||
@apply w-7 h-7 center-center bg-surface border text-secondary
|
||||
rounded mt-1 mr-1 shadow duration-200 hover:bg-surface-hover focus:bg-surface-hover;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
.component-wrapper {
|
||||
@apply rounded-md border overflow-hidden border-gray-300 dark:border-gray-600;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { LoaderIcon, RefreshCw } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
|
||||
export let id: string
|
||||
export let loading: boolean
|
||||
|
||||
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
|
||||
</script>
|
||||
|
||||
<Popover>
|
||||
<Button
|
||||
startIcon={{
|
||||
icon: loading ? LoaderIcon : RefreshCw,
|
||||
classes: twMerge(
|
||||
loading ? 'animate-spin text-blue-800' : '',
|
||||
'transition-all text-gray-500 dark:text-white'
|
||||
)
|
||||
}}
|
||||
color="light"
|
||||
size="xs2"
|
||||
btnClasses={twMerge(loading ? ' bg-blue-100 dark:bg-blue-400' : '', 'transition-all')}
|
||||
on:click={() => {
|
||||
$runnableComponents[id]?.cb?.map((cb) => cb())
|
||||
}}
|
||||
iconOnly
|
||||
/>
|
||||
<svelte:fragment slot="text">
|
||||
{#if loading}
|
||||
Refreshing...
|
||||
{:else}
|
||||
Refresh
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -39,6 +39,7 @@
|
||||
import { getSelectInput } from './queries/select'
|
||||
import DebouncedInput from '../../helpers/DebouncedInput.svelte'
|
||||
import { CancelablePromise } from '$lib/gen'
|
||||
import RefreshButton from '$lib/components/apps/components/RefreshButton.svelte'
|
||||
|
||||
export let id: string
|
||||
export let configuration: RichConfigurations
|
||||
@@ -570,6 +571,8 @@
|
||||
|
||||
$: hideSearch = resolvedConfig.hideSearch as boolean
|
||||
$: hideInsert = resolvedConfig.hideInsert as boolean
|
||||
|
||||
let loading: boolean = false
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['dbexplorercomponent'].initialData.configuration) as key (key)}
|
||||
@@ -623,35 +626,39 @@
|
||||
noInitialize
|
||||
bind:runnableComponent
|
||||
componentInput={input}
|
||||
autoRefresh={false}
|
||||
autoRefresh={true}
|
||||
bind:loading
|
||||
{render}
|
||||
{id}
|
||||
{outputs}
|
||||
>
|
||||
<div class="h-full" bind:clientHeight={componentContainerHeight}>
|
||||
{#if !(hideSearch === true && hideInsert === true)}
|
||||
<div class="flex p-2 justify-between gap-4" bind:clientHeight={buttonContainerHeight}>
|
||||
<div class="flex py-2 h-12 justify-between gap-4" bind:clientHeight={buttonContainerHeight}>
|
||||
{#if hideSearch !== true}
|
||||
<DebouncedInput
|
||||
class="w-full max-w-[300px]"
|
||||
type="text"
|
||||
bind:value={quicksearch}
|
||||
placeholder="Quicksearch"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
{/if}
|
||||
{#if hideInsert !== true}
|
||||
<Button
|
||||
startIcon={{ icon: Plus }}
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
args = {}
|
||||
insertDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
Insert
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-2">
|
||||
<RefreshButton {id} {loading} />
|
||||
{#if hideInsert !== true}
|
||||
<Button
|
||||
startIcon={{ icon: Plus }}
|
||||
color="dark"
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
args = {}
|
||||
insertDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
Insert
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.resource && resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table}
|
||||
|
||||
@@ -269,7 +269,7 @@ export function getSelectInput(
|
||||
},
|
||||
type: 'runnable',
|
||||
fieldType: 'object',
|
||||
hideRefreshButton: false
|
||||
hideRefreshButton: true
|
||||
}
|
||||
|
||||
return getQuery
|
||||
|
||||
+39
-5
@@ -17,9 +17,10 @@
|
||||
import type { InitConfig } from '$lib/components/apps/editor/appUtils'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { cellRendererFactory } from './utils'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
import { Columns, Trash2 } from 'lucide-svelte'
|
||||
import type { ColumnDef } from '../dbtable/utils'
|
||||
import AppAggridTableActions from './AppAggridTableActions.svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
|
||||
|
||||
export let id: string
|
||||
@@ -341,6 +342,10 @@
|
||||
api?.purgeInfiniteCache()
|
||||
}
|
||||
|
||||
export function restoreColumns() {
|
||||
api?.resetColumnState()
|
||||
}
|
||||
|
||||
function onSelectionChanged(api: GridApi<any>) {
|
||||
if (resolvedConfig?.multipleSelectable) {
|
||||
const rows = api.getSelectedNodes()
|
||||
@@ -391,7 +396,11 @@
|
||||
|
||||
{#if Array.isArray(resolvedConfig.columnDefs) && resolvedConfig.columnDefs.every(isObject)}
|
||||
<div
|
||||
class={twMerge('divide-y flex flex-col h-full', css?.container?.class, 'wm-aggrid-container')}
|
||||
class={twMerge(
|
||||
'flex flex-col h-full component-wrapper divide-y',
|
||||
css?.container?.class,
|
||||
'wm-aggrid-container'
|
||||
)}
|
||||
style={containerHeight ? `height: ${containerHeight}px;` : css?.container?.style}
|
||||
bind:clientHeight
|
||||
bind:clientWidth
|
||||
@@ -407,9 +416,23 @@
|
||||
>
|
||||
<div bind:this={eGui} style:height="100%" />
|
||||
</div>
|
||||
<div class="flex gap-1 w-full justify-end text-sm text-secondary py-1"
|
||||
>{firstRow}{'->'}{lastRow + 1} of {datasource?.rowCount} rows</div
|
||||
>
|
||||
<div class="flex gap-1 w-full justify-between items-center text-sm text-secondary/80 p-2">
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Restore columns</svelte:fragment>
|
||||
<Button
|
||||
startIcon={{ icon: Columns }}
|
||||
color="light"
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
// Restore the columnDefs to the original state
|
||||
restoreColumns()
|
||||
}}
|
||||
iconOnly
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
{firstRow}{'->'}{lastRow + 1} of {datasource?.rowCount} rows
|
||||
</div>
|
||||
</div>
|
||||
{:else if resolvedConfig.columnDefs != undefined}
|
||||
<Alert title="Parsing issues" type="error" size="xs">
|
||||
@@ -421,3 +444,14 @@
|
||||
{:else}
|
||||
<Alert title="Parsing issues" type="error" size="xs">The columnDefs are undefined</Alert>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.ag-theme-alpine {
|
||||
/* disable all borders */
|
||||
--ag-borders: none;
|
||||
--ag-row-border-style: solid;
|
||||
--ag-border-color: rgb(209 213 219);
|
||||
--ag-header-border-style: solid;
|
||||
--ag-border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import RefreshButton from '$lib/components/apps/components/RefreshButton.svelte'
|
||||
|
||||
import 'ag-grid-community/styles/ag-grid.css'
|
||||
import 'ag-grid-community/styles/ag-theme-alpine.css'
|
||||
import './theme/windmill-theme.css'
|
||||
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -389,6 +390,7 @@
|
||||
sendUserToast("Couldn't update the grid:" + e, true)
|
||||
}
|
||||
}
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['aggridcomponent'].initialData.configuration) as key (key)}
|
||||
@@ -410,19 +412,29 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
|
||||
<RunnableWrapper
|
||||
{outputs}
|
||||
{render}
|
||||
{componentInput}
|
||||
{id}
|
||||
bind:initializing
|
||||
bind:result
|
||||
bind:loading
|
||||
hideRefreshButton={true}
|
||||
>
|
||||
{#if Array.isArray(value) && value.every(isObject)}
|
||||
{#if Array.isArray(resolvedConfig.columnDefs) && resolvedConfig.columnDefs.every(isObject)}
|
||||
<div
|
||||
class={twMerge(
|
||||
'border shadow-sm divide-y flex flex-col h-full',
|
||||
css?.container?.class,
|
||||
'wm-aggrid-container'
|
||||
)}
|
||||
class={twMerge('flex flex-col h-full', css?.container?.class, 'wm-aggrid-container')}
|
||||
style={css?.container?.style}
|
||||
bind:clientHeight
|
||||
bind:clientWidth
|
||||
>
|
||||
<div class="py-1 flex flex-row justify-end items-center">
|
||||
{#if componentInput?.type === 'runnable' && componentInput.autoRefresh}
|
||||
<RefreshButton {id} {loading} />
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
on:pointerdown|stopPropagation={() => {
|
||||
$selectedComponent = [id]
|
||||
|
||||
@@ -41,9 +41,10 @@
|
||||
import ResolveStyle from '../../helpers/ResolveStyle.svelte'
|
||||
import { Popup } from '$lib/components/common'
|
||||
import ComponentOutputViewer from '$lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte'
|
||||
import { Plug2 } from 'lucide-svelte'
|
||||
import { EyeIcon, Plug2 } from 'lucide-svelte'
|
||||
import AppCell from './AppCell.svelte'
|
||||
import sum from 'hash-sum'
|
||||
import RefreshButton from '$lib/components/apps/components/RefreshButton.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -344,243 +345,326 @@
|
||||
bind:loading
|
||||
>
|
||||
{#if Array.isArray(result) && result.every(isObject)}
|
||||
<div
|
||||
class={twMerge(
|
||||
'border shadow-sm divide-y h-full',
|
||||
css?.container?.class ?? '',
|
||||
'wm-table-container',
|
||||
'flex flex-col'
|
||||
)}
|
||||
style={css?.container?.style ?? ''}
|
||||
>
|
||||
{#if resolvedConfig.search !== 'Disabled'}
|
||||
<div class="px-2 py-1">
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col h-full">
|
||||
{#if resolvedConfig.search !== 'Disabled' || (componentInput?.hideRefreshButton && componentInput['autoRefresh'])}
|
||||
<div class="flex flex-row w-full justify-between items-center h-12">
|
||||
{#if resolvedConfig.search !== 'Disabled'}
|
||||
<div class="grow max-w-[300px]">
|
||||
<DebouncedInput placeholder="Search..." bind:value={searchValue} />
|
||||
<DebouncedInput
|
||||
placeholder="Search..."
|
||||
bind:value={searchValue}
|
||||
parentClass="h-full "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
|
||||
{#if componentInput?.hideRefreshButton && componentInput['autoRefresh']}
|
||||
<RefreshButton {id} {loading} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="overflow-x-auto flex-1 w-full">
|
||||
<table class="relative w-full border-b">
|
||||
<thead
|
||||
class={twMerge(
|
||||
'bg-surface-secondary text-left',
|
||||
css?.tableHeader?.class ?? '',
|
||||
'wm-table-header',
|
||||
'sticky top-0 z-40'
|
||||
)}
|
||||
style={css?.tableHeader?.style ?? ''}
|
||||
>
|
||||
{#each getHeaderGroups($table) as headerGroup}
|
||||
<tr class="divide-x">
|
||||
{#each headerGroup.headers as header}
|
||||
{#if header?.column?.columnDef?.header}
|
||||
{@const context = header?.getContext()}
|
||||
{#if context}
|
||||
{@const component = renderCell(header.column.columnDef.header, context)}
|
||||
{@const displayName = getDisplayNameById(header.id)}
|
||||
<th class="!p-0">
|
||||
<span class="block px-4 py-4 text-sm font-semibold border-b">
|
||||
{#if displayName}
|
||||
{displayName}
|
||||
{:else if !header.isPlaceholder && component}
|
||||
<svelte:component this={component} />
|
||||
{/if}
|
||||
</span>
|
||||
</th>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{#if actionButtons.length > 0}
|
||||
<th class="!p-0">
|
||||
<span class="block px-4 py-4 text-sm font-semibold border-b"> Actions </span>
|
||||
</th>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</thead>
|
||||
<tbody
|
||||
class={twMerge('divide-y bg-surface', css?.tableBody?.class ?? '', 'wm-table-body')}
|
||||
style={css?.tableBody?.style ?? ''}
|
||||
>
|
||||
{#each $table.getRowModel().rows as row (row.id)}
|
||||
{@const rowIndex = row.original['__index']}
|
||||
<tr
|
||||
class={classNames(
|
||||
'last-of-type:!border-b-0 divide-x w-full',
|
||||
selectedRowIndex === rowIndex
|
||||
? 'bg-blue-100 hover:bg-blue-200 dark:bg-surface-selected dark:hover:bg-surface-hover divide-blue-200 hover:divide-blue-300 dark:divide-gray-600 dark:hover:divide-gray-700 wm-table-row-selected'
|
||||
: 'hover:bg-blue-50 dark:hover:bg-surface-hover wm-table-row'
|
||||
)}
|
||||
>
|
||||
{#each safeVisibleCell(row) as cell, index (index)}
|
||||
{#if cell?.column?.columnDef?.cell}
|
||||
{@const context = cell?.getContext()}
|
||||
{#if context}
|
||||
<AppCell
|
||||
on:keydown={() => toggleRow(row)}
|
||||
on:click={() => toggleRow(row)}
|
||||
type={resolvedConfig.columnDefs?.find(
|
||||
// TS types are wrong here
|
||||
// @ts-ignore
|
||||
(c) => c.field === cell.column.columnDef.accessorKey
|
||||
)?.type ?? 'text'}
|
||||
value={cell.getValue()}
|
||||
width={cell.column.getSize()}
|
||||
on:update={(event) => {
|
||||
updateCellValue(rowIndex, index, event.detail.value)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if actionButtons.length > 0}
|
||||
<td
|
||||
class="p-2"
|
||||
on:keypress={() => toggleRow(row)}
|
||||
on:click={() => toggleRow(row)}
|
||||
style="width: {(actionButtons ?? []).length * 130}px"
|
||||
>
|
||||
<div class="center-center h-full w-full flex-wrap gap-2">
|
||||
{#each actionButtons as actionButton, actionIndex (actionButton?.id)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<RowWrapper
|
||||
value={row.original}
|
||||
index={rowIndex}
|
||||
onSet={(id, value) => {
|
||||
if (!inputs[id]) {
|
||||
inputs[id] = { [rowIndex]: value }
|
||||
} else {
|
||||
inputs[id] = { ...inputs[id], [rowIndex]: value }
|
||||
}
|
||||
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
onRemove={(id) => {
|
||||
if (inputs?.[id] == undefined) {
|
||||
return
|
||||
}
|
||||
delete inputs[id][rowIndex]
|
||||
inputs[id] = { ...inputs[id] }
|
||||
if (Object.keys(inputs?.[id] ?? {}).length == 0) {
|
||||
delete inputs[id]
|
||||
inputs = { ...inputs }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'component-wrapper bg-surface',
|
||||
'divide-y h-full',
|
||||
css?.container?.class ?? '',
|
||||
'wm-table-container',
|
||||
'flex flex-col'
|
||||
)}
|
||||
style={css?.container?.style ?? ''}
|
||||
>
|
||||
<div class="overflow-x-auto flex-1 w-full">
|
||||
<table class="relative w-full border-b">
|
||||
<thead
|
||||
class={twMerge(
|
||||
'bg-surface-secondary text-left',
|
||||
css?.tableHeader?.class ?? '',
|
||||
'wm-table-header',
|
||||
'sticky top-0 z-40'
|
||||
)}
|
||||
style={css?.tableHeader?.style ?? ''}
|
||||
>
|
||||
{#each getHeaderGroups($table) as headerGroup}
|
||||
<tr class="divide-x">
|
||||
{#each headerGroup.headers as header}
|
||||
{#if header?.column?.columnDef?.header}
|
||||
{@const context = header?.getContext()}
|
||||
{#if context}
|
||||
{@const component = renderCell(header.column.columnDef.header, context)}
|
||||
{@const displayName = getDisplayNameById(header.id)}
|
||||
<th class="!p-0">
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
if (actionButton.id !== $hoverStore) {
|
||||
$hoverStore = actionButton.id
|
||||
}
|
||||
}}
|
||||
on:mouseout|stopPropagation={() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
$hoverStore = undefined
|
||||
}
|
||||
}}
|
||||
class={classNames(
|
||||
($selectedComponent?.includes(actionButton.id) ||
|
||||
$hoverStore === actionButton.id) &&
|
||||
$mode !== 'preview'
|
||||
? 'outline outline-indigo-500 outline-1 outline-offset-1 relative'
|
||||
: 'relative'
|
||||
)}
|
||||
class="flex flex-row items-center gap-1 px-4 py-4 text-xs text-primary font-medium border-b"
|
||||
>
|
||||
{#if $mode !== 'preview'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span
|
||||
title={`Id: ${actionButton.id}`}
|
||||
class={classNames(
|
||||
'px-2 text-2xs font-bold w-fit absolute shadow -top-2 -left-2 border z-50 rounded-sm',
|
||||
'bg-indigo-500/90 border-indigo-600 text-white',
|
||||
$selectedComponent?.includes(actionButton.id) ||
|
||||
$hoverStore === actionButton.id
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
on:click|stopPropagation={() => {
|
||||
$selectedComponent = [actionButton.id]
|
||||
{#if displayName}
|
||||
{displayName}
|
||||
{:else if !header.isPlaceholder && component}
|
||||
<svelte:component this={component} />
|
||||
{/if}
|
||||
{#if header.column.getIsVisible()}
|
||||
<button
|
||||
class="w-6 flex justify-center items-center"
|
||||
on:click={() => {
|
||||
header.column.toggleVisibility()
|
||||
}}
|
||||
>
|
||||
{actionButton.id}
|
||||
</span>
|
||||
|
||||
{#if $connectingInput.opened}
|
||||
<div class="absolute z-50 left-8 -top-[10px]">
|
||||
<Popup
|
||||
floatingConfig={{
|
||||
strategy: 'absolute',
|
||||
placement: 'bottom-start'
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<button
|
||||
class="bg-red-500/70 border border-red-600 px-1 py-0.5"
|
||||
title="Outputs"
|
||||
aria-label="Open output"><Plug2 size={12} /></button
|
||||
>
|
||||
</svelte:fragment>
|
||||
<ComponentOutputViewer
|
||||
suffix="table"
|
||||
on:select={({ detail }) =>
|
||||
connectOutput(
|
||||
connectingInput,
|
||||
'buttoncomponent',
|
||||
actionButton.id,
|
||||
detail
|
||||
)}
|
||||
componentId={actionButton.id}
|
||||
/>
|
||||
</Popup>
|
||||
</div>
|
||||
{/if}
|
||||
<EyeIcon
|
||||
size="14"
|
||||
class="hover:text-gray-600 text-gray-400 rounded-full "
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
{#if rowIndex == 0}
|
||||
{@const controls = {
|
||||
left: () => {
|
||||
if (actionIndex === 0) {
|
||||
$selectedComponent = [id]
|
||||
return true
|
||||
} else if (actionIndex > 0) {
|
||||
$selectedComponent = [actionButtons[actionIndex - 1].id]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
right: () => {
|
||||
if (actionIndex === actionButtons.length - 1) {
|
||||
return id
|
||||
} else if (actionIndex < actionButtons.length - 1) {
|
||||
$selectedComponent = [actionButtons[actionIndex + 1].id]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
</div>
|
||||
</th>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{#if actionButtons.length > 0}
|
||||
<th class="!p-0">
|
||||
<div
|
||||
class="flex flex-row items-center px-4 py-4 text-xs text-primary font-medium border-b"
|
||||
>
|
||||
Actions
|
||||
</div>
|
||||
</th>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</thead>
|
||||
<tbody
|
||||
class={twMerge('divide-y bg-surface', css?.tableBody?.class ?? '', 'wm-table-body')}
|
||||
style={css?.tableBody?.style ?? ''}
|
||||
>
|
||||
{#each $table.getRowModel().rows as row, index (row.id)}
|
||||
{@const isLastRow = index === filteredResult.length - 1}
|
||||
{@const rowIndex = row.original['__index']}
|
||||
<tr
|
||||
class={classNames(
|
||||
isLastRow ? '!border-b-0' : '',
|
||||
'divide-x w-full',
|
||||
index % 2 === 0 ? 'bg-gray-50/50' : '',
|
||||
selectedRowIndex === rowIndex
|
||||
? 'bg-blue-100 hover:bg-blue-200 dark:bg-surface-selected dark:hover:bg-surface-hover divide-blue-200 hover:divide-blue-300 dark:divide-gray-600 dark:hover:divide-gray-700 wm-table-row-selected'
|
||||
: 'hover:bg-blue-50 dark:hover:bg-surface-hover wm-table-row'
|
||||
)}
|
||||
>
|
||||
{#each safeVisibleCell(row) as cell, index (index)}
|
||||
{#if cell?.column?.columnDef?.cell}
|
||||
{@const context = cell?.getContext()}
|
||||
{#if context}
|
||||
<AppCell
|
||||
on:keydown={() => toggleRow(row)}
|
||||
on:click={() => toggleRow(row)}
|
||||
type={resolvedConfig.columnDefs?.find(
|
||||
// TS types are wrong here
|
||||
// @ts-ignore
|
||||
(c) => c.field === cell.column.columnDef.accessorKey
|
||||
)?.type ?? 'text'}
|
||||
value={cell.getValue()}
|
||||
width={cell.column.getSize()}
|
||||
on:update={(event) => {
|
||||
updateCellValue(rowIndex, index, event.detail.value)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if actionButtons.length > 0}
|
||||
<td
|
||||
class="p-2"
|
||||
on:keypress={() => toggleRow(row)}
|
||||
on:click={() => toggleRow(row)}
|
||||
style="width: {(actionButtons ?? []).length * 130}px"
|
||||
>
|
||||
<div class="center-center h-full w-full flex-wrap gap-2">
|
||||
{#each actionButtons as actionButton, actionIndex (actionButton?.id)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<RowWrapper
|
||||
value={row.original}
|
||||
index={rowIndex}
|
||||
onSet={(id, value) => {
|
||||
if (!inputs[id]) {
|
||||
inputs[id] = { [rowIndex]: value }
|
||||
} else {
|
||||
inputs[id] = { ...inputs[id], [rowIndex]: value }
|
||||
}
|
||||
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
onRemove={(id) => {
|
||||
if (inputs?.[id] == undefined) {
|
||||
return
|
||||
}
|
||||
delete inputs[id][rowIndex]
|
||||
inputs[id] = { ...inputs[id] }
|
||||
if (Object.keys(inputs?.[id] ?? {}).length == 0) {
|
||||
delete inputs[id]
|
||||
inputs = { ...inputs }
|
||||
}
|
||||
outputs?.inputs.set(inputs, true)
|
||||
}}
|
||||
>
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<div
|
||||
on:mouseover|stopPropagation={() => {
|
||||
if (actionButton.id !== $hoverStore) {
|
||||
$hoverStore = actionButton.id
|
||||
}
|
||||
}}
|
||||
{#if actionButton.type == 'buttoncomponent'}
|
||||
on:mouseout|stopPropagation={() => {
|
||||
if ($hoverStore !== undefined) {
|
||||
$hoverStore = undefined
|
||||
}
|
||||
}}
|
||||
class={classNames(
|
||||
($selectedComponent?.includes(actionButton.id) ||
|
||||
$hoverStore === actionButton.id) &&
|
||||
$mode !== 'preview'
|
||||
? 'outline outline-indigo-500 outline-1 outline-offset-1 relative'
|
||||
: 'relative'
|
||||
)}
|
||||
>
|
||||
{#if $mode !== 'preview'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<span
|
||||
title={`Id: ${actionButton.id}`}
|
||||
class={classNames(
|
||||
'px-2 text-2xs font-bold w-fit absolute shadow -top-2 -left-2 border z-50 rounded-sm',
|
||||
'bg-indigo-500/90 border-indigo-600 text-white',
|
||||
$selectedComponent?.includes(actionButton.id) ||
|
||||
$hoverStore === actionButton.id
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
on:click|stopPropagation={() => {
|
||||
$selectedComponent = [actionButton.id]
|
||||
}}
|
||||
>
|
||||
{actionButton.id}
|
||||
</span>
|
||||
|
||||
{#if $connectingInput.opened}
|
||||
<div class="absolute z-50 left-8 -top-[10px]">
|
||||
<Popup
|
||||
floatingConfig={{
|
||||
strategy: 'absolute',
|
||||
placement: 'bottom-start'
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<button
|
||||
class="bg-red-500/70 border border-red-600 px-1 py-0.5"
|
||||
title="Outputs"
|
||||
aria-label="Open output"><Plug2 size={12} /></button
|
||||
>
|
||||
</svelte:fragment>
|
||||
<ComponentOutputViewer
|
||||
suffix="table"
|
||||
on:select={({ detail }) =>
|
||||
connectOutput(
|
||||
connectingInput,
|
||||
'buttoncomponent',
|
||||
actionButton.id,
|
||||
detail
|
||||
)}
|
||||
componentId={actionButton.id}
|
||||
/>
|
||||
</Popup>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if rowIndex == 0}
|
||||
{@const controls = {
|
||||
left: () => {
|
||||
if (actionIndex === 0) {
|
||||
$selectedComponent = [id]
|
||||
return true
|
||||
} else if (actionIndex > 0) {
|
||||
$selectedComponent = [actionButtons[actionIndex - 1].id]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
right: () => {
|
||||
if (actionIndex === actionButtons.length - 1) {
|
||||
return id
|
||||
} else if (actionIndex < actionButtons.length - 1) {
|
||||
$selectedComponent = [actionButtons[actionIndex + 1].id]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}}
|
||||
{#if actionButton.type == 'buttoncomponent'}
|
||||
<AppButton
|
||||
noInitialize
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
noWFull
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
extraQueryParams={{ row: row.original }}
|
||||
componentInput={actionButton.componentInput}
|
||||
{controls}
|
||||
/>
|
||||
{:else if actionButton.type == 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
noInitialize
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
onToggle={actionButton.onToggle}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
{controls}
|
||||
/>
|
||||
{:else if actionButton.type == 'selectcomponent'}
|
||||
<div class="w-40">
|
||||
<AppSelect
|
||||
noDefault
|
||||
noInitialize
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
onSelect={actionButton.onSelect}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
{controls}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if actionButton.type == 'buttoncomponent'}
|
||||
<AppButton
|
||||
noInitialize
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
noWFull
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
extraQueryParams={{ row: row.original }}
|
||||
componentInput={actionButton.componentInput}
|
||||
{controls}
|
||||
/>
|
||||
{:else if actionButton.type == 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
@@ -595,7 +679,6 @@
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
{controls}
|
||||
/>
|
||||
{:else if actionButton.type == 'selectcomponent'}
|
||||
<div class="w-40">
|
||||
@@ -612,80 +695,32 @@
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
{controls}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if actionButton.type == 'buttoncomponent'}
|
||||
<AppButton
|
||||
noInitialize
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
noWFull
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
extraQueryParams={{ row: row.original }}
|
||||
componentInput={actionButton.componentInput}
|
||||
/>
|
||||
{:else if actionButton.type == 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
noInitialize
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
onToggle={actionButton.onToggle}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
/>
|
||||
{:else if actionButton.type == 'selectcomponent'}
|
||||
<div class="w-40">
|
||||
<AppSelect
|
||||
noDefault
|
||||
noInitialize
|
||||
extraKey={'idx' + rowIndex}
|
||||
{render}
|
||||
id={actionButton.id}
|
||||
customCss={actionButton.customCss}
|
||||
configuration={actionButton.configuration}
|
||||
recomputeIds={actionButton.recomputeIds}
|
||||
onSelect={actionButton.onSelect}
|
||||
preclickAction={async () => {
|
||||
toggleRow(row)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</RowWrapper>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</RowWrapper>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<AppTableFooter
|
||||
download={resolvedConfig?.downloadButton}
|
||||
pageSize={resolvedConfig?.pagination?.configuration?.auto?.pageSize ?? 20}
|
||||
manualPagination={resolvedConfig?.pagination?.selected == 'manual'}
|
||||
result={filteredResult}
|
||||
{table}
|
||||
class={twMerge(css?.tableFooter?.class, 'wm-table-footer')}
|
||||
style={css?.tableFooter?.style}
|
||||
{loading}
|
||||
/>
|
||||
<AppTableFooter
|
||||
download={resolvedConfig?.downloadButton}
|
||||
pageSize={resolvedConfig?.pagination?.configuration?.auto?.pageSize ?? 20}
|
||||
manualPagination={resolvedConfig?.pagination?.selected == 'manual'}
|
||||
result={filteredResult}
|
||||
{table}
|
||||
class={twMerge(css?.tableFooter?.class, 'wm-table-footer')}
|
||||
style={css?.tableFooter?.style}
|
||||
{loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if result != undefined}
|
||||
<div class="flex flex-col h-full w-full overflow-auto">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import type { Table } from '@tanstack/svelte-table'
|
||||
import { ChevronLeft, ChevronRight, Download } from 'lucide-svelte'
|
||||
import { ChevronLeft, ChevronRight, Columns, Download } from 'lucide-svelte'
|
||||
import type { Readable } from 'svelte/store'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
@@ -11,11 +12,12 @@
|
||||
export let manualPagination: boolean
|
||||
export let pageSize: number
|
||||
export let table: Readable<Table<T>>
|
||||
export let download: boolean = true
|
||||
export let loading: boolean = false
|
||||
|
||||
let c = ''
|
||||
export { c as class }
|
||||
export let style = ''
|
||||
export let download: boolean = true
|
||||
export let loading: boolean = false
|
||||
|
||||
function convertJSONToCSV(objArray: Record<string, any>[]) {
|
||||
let str = ''
|
||||
@@ -60,6 +62,37 @@
|
||||
class={twMerge('px-2 py-1 text-xs gap-2 items-center justify-between', c, 'flex flex-row')}
|
||||
{style}
|
||||
>
|
||||
<div class="flex items-center gap-1 flex-row">
|
||||
{#if download}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Download as CSV</svelte:fragment>
|
||||
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={downloadResultAsCSV}
|
||||
startIcon={{ icon: Download }}
|
||||
wrapperClasses="app-table-footer-btn"
|
||||
iconOnly
|
||||
/>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if !$table.getIsAllColumnsVisible()}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Display hidden columns</svelte:fragment>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
on:click={() => {
|
||||
$table.getAllColumns().forEach((column) => column.toggleVisibility(true))
|
||||
}}
|
||||
startIcon={{ icon: Columns }}
|
||||
wrapperClasses="app-table-footer-btn"
|
||||
iconOnly
|
||||
/>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{#if result.length > pageSize || manualPagination}
|
||||
<div class="flex items-center gap-2 flex-row">
|
||||
<Button
|
||||
@@ -77,6 +110,7 @@
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="xs2"
|
||||
variant="border"
|
||||
@@ -92,26 +126,12 @@
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
{$table.getState().pagination.pageIndex + 1}
|
||||
{$table.getPageCount() > 0 ? ` of ${$table.getPageCount()}` : ''}
|
||||
<div>
|
||||
Page:
|
||||
{$table.getState().pagination.pageIndex + 1}
|
||||
{$table.getPageCount() > 0 ? ` of ${$table.getPageCount()}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
<div class="flex items-center gap-2 flex-row">
|
||||
{#if download}
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
color="light"
|
||||
btnClasses="!py-1"
|
||||
on:click={downloadResultAsCSV}
|
||||
startIcon={{ icon: Download }}
|
||||
wrapperClasses="app-table-footer-btn"
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let placeholder: string = 'Search...'
|
||||
// Using 'any' so 'type="number"' can be passed to the input
|
||||
// which should return a number
|
||||
@@ -25,6 +27,6 @@
|
||||
on:pointerdown|stopPropagation
|
||||
on:keyup={debounce}
|
||||
on:keydown|stopPropagation
|
||||
class={parentClass}
|
||||
class={twMerge(parentClass, 'mb-1 h-8 !rounded-md !shadow-none')}
|
||||
{...$$restProps}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
export let componentInput: AppInput | undefined
|
||||
export let noInitialize = false
|
||||
export let hideRefreshButton: boolean | undefined = undefined
|
||||
|
||||
type SideEffectAction =
|
||||
| {
|
||||
@@ -251,7 +252,7 @@
|
||||
fields={componentInput.fields}
|
||||
bind:result
|
||||
runnable={componentInput.runnable}
|
||||
hideRefreshButton={componentInput.hideRefreshButton}
|
||||
hideRefreshButton={componentInput.hideRefreshButton ?? hideRefreshButton}
|
||||
transformer={componentInput.transformer}
|
||||
{autoRefresh}
|
||||
recomputableByRefreshButton={componentInput.autoRefresh ?? true}
|
||||
|
||||
@@ -1726,7 +1726,8 @@ This is a paragraph.
|
||||
name: 'A briefer cell',
|
||||
age: 84
|
||||
}
|
||||
]
|
||||
],
|
||||
hideRefreshButton: true
|
||||
} as StaticAppInput,
|
||||
actionButtons: true
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
componentSettings?.forEach((componentSetting) => {
|
||||
const id = componentSetting?.item?.id
|
||||
|
||||
const onDeleteComponentControl = id ? $componentControl[id]?.onDelete : undefined
|
||||
if (onDeleteComponentControl) {
|
||||
onDeleteComponentControl()
|
||||
|
||||
@@ -170,6 +170,7 @@ type InputConfiguration<T extends InputType, V extends InputType> = {
|
||||
}
|
||||
noStatic?: boolean
|
||||
onDemandOnly?: boolean
|
||||
hideRefreshButton?: boolean
|
||||
}
|
||||
|
||||
export type StaticOptions = {
|
||||
|
||||
Reference in New Issue
Block a user