diff --git a/frontend/src/lib/components/apps/components/display/table/AppAggridTable.svelte b/frontend/src/lib/components/apps/components/display/table/AppAggridTable.svelte index 588fe546d6..bd7e73cf5a 100644 --- a/frontend/src/lib/components/apps/components/display/table/AppAggridTable.svelte +++ b/frontend/src/lib/components/apps/components/display/table/AppAggridTable.svelte @@ -40,7 +40,7 @@ import ResolveStyle from '../../helpers/ResolveStyle.svelte' import AppAggridTableActions from './AppAggridTableActions.svelte' - import { cellRendererFactory, defaultCellRenderer } from './utils' + import { cellRendererFactory, transformColumnDefs, type WindmillColumnDef } from './utils' import Popover from '$lib/components/Popover.svelte' import { Button } from '$lib/components/common' import InputValue from '../../helpers/InputValue.svelte' @@ -303,6 +303,12 @@ taComponent.props.rowIndex = params.node.rowIndex ?? 0 taComponent.props.row = params.data taComponent.props.p = params + const nextActions: TableAction[] | undefined = computedOrder + ? (computedOrder + .map((key) => actions?.find((a) => a.id === key)) + .filter(Boolean) as TableAction[]) + : actions + taComponent.props.actions = nextActions } } }) @@ -317,36 +323,25 @@ // console.log(resolvedConfig?.extraConfig) if (eGui) { try { - let columnDefs = - Array.isArray(resolvedConfig?.columnDefs) && resolvedConfig.columnDefs.every(isObject) - ? [...resolvedConfig?.columnDefs] // Clone to avoid direct mutation - : [] - - // Add the action column if actions are defined - if (actions && actions.length > 0) { - columnDefs.push({ - headerName: resolvedConfig?.customActionsHeader - ? resolvedConfig?.customActionsHeader - : 'Actions', - cellRenderer: tableActionsFactory, - autoHeight: true, - cellStyle: { textAlign: 'center' }, - cellClass: 'grid-cell-centered', - ...(!resolvedConfig?.wrapActions ? { minWidth: 130 * actions?.length } : {}) - }) - } + const agColumnDefs = transformColumnDefs({ + columnDefs: + Array.isArray(resolvedConfig?.columnDefs) && resolvedConfig.columnDefs.every(isObject) + ? [...resolvedConfig?.columnDefs] as WindmillColumnDef[] + : [], + actions, + customActionsHeader: resolvedConfig?.customActionsHeader, + wrapActions: resolvedConfig?.wrapActions, + tableActionsFactory, + onInvalidColumnDefs: (errors) => { + sendUserToast(`Invalid columnDefs: ${errors.join('\n')}`, true) + } + }) createGrid( eGui, { rowData: value, - columnDefs: columnDefs.map((fields) => { - let cr = defaultCellRenderer(fields.cellRendererType) - return { - ...fields, - ...(cr ? { cellRenderer: cr } : {}) - } - }), + columnDefs: agColumnDefs, pagination: resolvedConfig?.pagination, paginationAutoPageSize: resolvedConfig?.pagination, suppressPaginationPanel: true, @@ -476,34 +471,23 @@ function updateOptions() { try { - const columnDefs = - Array.isArray(resolvedConfig?.columnDefs) && resolvedConfig.columnDefs.every(isObject) - ? [...resolvedConfig?.columnDefs] // Clone to avoid direct mutation - : [] - - // Add the action column if actions are defined - if (actions && actions.length > 0) { - columnDefs.push({ - headerName: resolvedConfig?.customActionsHeader - ? resolvedConfig?.customActionsHeader - : 'Actions', - cellRenderer: tableActionsFactory, - autoHeight: true, - cellStyle: { textAlign: 'center' }, - cellClass: 'grid-cell-centered', - ...(!resolvedConfig?.wrapActions ? { minWidth: 130 * actions?.length } : {}) - }) - } + const agColumnDefs = transformColumnDefs({ + columnDefs: + Array.isArray(resolvedConfig?.columnDefs) && resolvedConfig.columnDefs.every(isObject) + ? [...resolvedConfig?.columnDefs] as WindmillColumnDef[] + : [], + actions, + customActionsHeader: resolvedConfig?.customActionsHeader, + wrapActions: resolvedConfig?.wrapActions, + tableActionsFactory, + onInvalidColumnDefs: (errors) => { + sendUserToast(`Invalid columnDefs: ${errors.join('\n')}`, true) + } + }) api?.updateGridOptions({ rowData: value, - columnDefs: columnDefs.map((fields) => { - let cr = defaultCellRenderer(fields.cellRendererType) - return { - ...fields, - ...(cr ? { cellRenderer: cr } : {}) - } - }), + columnDefs: agColumnDefs, pagination: resolvedConfig?.pagination, paginationAutoPageSize: resolvedConfig?.pagination, suppressPaginationPanel: true, @@ -523,6 +507,10 @@ ...resolvedConfig?.extraConfig?.['defaultColDef'] } }) + // Force refresh to re-render cell renderers after actions change + api?.refreshCells({ force: true }) + // Force complete redraw to clear stale inline styles + api?.redrawRows() } catch (e) { console.error(e) sendUserToast("Couldn't update the grid:" + e, true) @@ -609,7 +597,13 @@ bind:loading hideRefreshButton={true} > - + 0} + customActionsHeader={resolvedConfig?.customActionsHeader} + >
- result?: Array | undefined + columnDefs?: WindmillColumnDef[] + result?: Array> | undefined allowColumnDefsActions?: boolean children?: import('svelte').Snippet + actionsPresent?: boolean + customActionsHeader?: string | undefined } let { @@ -21,31 +28,113 @@ columnDefs = [], result = [], allowColumnDefsActions = true, - children + children, + actionsPresent = false, + customActionsHeader = undefined }: Props = $props() const { app, mode, selectedComponent } = getContext('AppViewerContext') + let syncInProgress = false + + function hasActionsPlaceholder(cols: WindmillColumnDef[] | undefined): boolean { + if (!Array.isArray(cols)) return false + return cols.findIndex((c) => c?._isActionsColumn === true) > -1 + } + + function addActionsPlaceholder(cols: WindmillColumnDef[] | undefined): WindmillColumnDef[] { + const hdr = customActionsHeader ?? 'Actions' + const placeholder: WindmillColumnDef = { + field: '__actions__', + _isActionsColumn: true, + headerName: hdr, + flex: 1 + } + if (!Array.isArray(cols)) return [placeholder] + return [...cols, placeholder] + } + + function removeActionsPlaceholder(cols: WindmillColumnDef[] | undefined): WindmillColumnDef[] { + if (!Array.isArray(cols)) return [] + return cols.filter((c) => c?._isActionsColumn !== true) + } + + async function ensureActionsColumn() { + // Only act in editor (DND) mode + if ($mode !== 'dnd') return + const gridItem = findGridItem($app, id) + if (!gridItem) return + + // Type the configuration more safely + const rawConf = gridItem.data.configuration?.columnDefs + if (!rawConf) return + + // Type guard for configuration structure + const conf = rawConf as ColumnDefsConfiguration + if (!conf.type || (conf.type !== 'static' && conf.type !== 'evalv2')) return + + let currentColumns: WindmillColumnDef[] | undefined + if (conf.type === 'static') { + currentColumns = Array.isArray(conf.value) ? conf.value : [] + } else if (conf.type === 'evalv2') { + try { + const parsed = JSON.parse(conf.expr ?? '[]') + currentColumns = Array.isArray(parsed) ? parsed : [] + } catch (e) { + console.warn('Failed to parse columnDefs expression:', e) + currentColumns = [] + } + } + + const hasPlaceholder = hasActionsPlaceholder(currentColumns) + + // Auto-sync logic: add missing actions column, remove when actions gone + const needsAdd = actionsPresent && !hasPlaceholder + const needsRemove = !actionsPresent && hasPlaceholder + + if (!needsAdd && !needsRemove) return + + let nextColumns = currentColumns || [] + if (needsAdd) nextColumns = addActionsPlaceholder(nextColumns) + if (needsRemove) nextColumns = removeActionsPlaceholder(nextColumns) + + // Update configuration with proper typing + if (conf.type === 'static') { + conf.value = nextColumns + } else if (conf.type === 'evalv2') { + conf.expr = JSON.stringify(nextColumns) + } + + await updateConfiguration() + } + + $effect(() => { + const shouldSync = actionsPresent !== undefined || columnDefs?.length !== undefined + if (shouldSync && !syncInProgress) { + syncInProgress = true + ensureActionsColumn().finally(() => { + syncInProgress = false + }) + } + }) + async function syncColumns() { - let gridItem = findGridItem($app, id) + const gridItem = findGridItem($app, id) if (gridItem && result) { const keys = Object.keys(result[0] ?? {}) ?? [] + const conf = gridItem.data.configuration.columnDefs as ColumnDefsConfiguration - if (gridItem.data.configuration.columnDefs.type === 'static') { - gridItem.data.configuration.columnDefs.value = keys.map((key) => ({ - field: key, - headerName: key, - flex: 1 - })) - } else if (gridItem.data.configuration.columnDefs.type === 'evalv2') { - gridItem.data.configuration.columnDefs.expr = JSON.stringify( - keys.map((key, index) => ({ - field: key, - headerName: key, - flex: 1 - })) - ) + const newColumns: WindmillColumnDef[] = keys.map((key) => ({ + field: key, + headerName: key, + flex: 1 + })) + + if (conf.type === 'static') { + conf.value = newColumns + } else if (conf.type === 'evalv2') { + conf.expr = JSON.stringify(newColumns) } await updateConfiguration() @@ -53,12 +142,16 @@ } async function setEmptyColumns() { - let gridItem = findGridItem($app, id) - if (gridItem && gridItem.data.configuration.columnDefs.type === 'static') { - gridItem.data.configuration.columnDefs.value = [] + const gridItem = findGridItem($app, id) + if (!gridItem) return + + const conf = gridItem.data.configuration.columnDefs as ColumnDefsConfiguration + + if (conf.type === 'static') { + conf.value = [] await updateConfiguration() - } else if (gridItem && gridItem.data.configuration.columnDefs.type === 'evalv2') { - gridItem.data.configuration.columnDefs.expr = '[]' + } else if (conf.type === 'evalv2') { + conf.expr = '[]' await updateConfiguration() } } diff --git a/frontend/src/lib/components/apps/components/display/table/utils.ts b/frontend/src/lib/components/apps/components/display/table/utils.ts index 6a9d3f0d9d..3f84b66db5 100644 --- a/frontend/src/lib/components/apps/components/display/table/utils.ts +++ b/frontend/src/lib/components/apps/components/display/table/utils.ts @@ -3,12 +3,21 @@ * See: https://stackoverflow.com/a/72608215 */ import type { ColDef, ColGroupDef, ICellRendererComp, ICellRendererParams } from 'ag-grid-community' -import { ColumnIdentity, type ColumnDef } from '../dbtable/utils' +import { ColumnIdentity } from '../dbtable/utils' import type { TableAction } from '$lib/components/apps/editor/component' import { mount, unmount } from 'svelte' import { Button } from '$lib/components/common' import { Trash2 } from 'lucide-svelte' +export type WindmillColumnDef = ColDef & { + _isActionsColumn?: boolean + cellRendererType?: string + ignored?: boolean + hideInsert?: boolean + isidentity?: ColumnIdentity + children?: WindmillColumnDef[] +} + /** * Class for defining a cell renderer. * If you don't need to define a separate class you could use cellRendererFactory @@ -125,7 +134,7 @@ export function transformColumnDefs({ onDelete, onInvalidColumnDefs }: { - columnDefs: ColumnDef[] + columnDefs: WindmillColumnDef[] actions?: TableAction[] customActionsHeader?: string wrapActions?: boolean @@ -146,6 +155,13 @@ export function transformColumnDefs({ let r: any[] = columnDefs?.filter((x) => x && !x.ignored) ?? [] + // Allow an explicit "actions" placeholder in columnDefs so users can manage it like any column. + // When present, replace it with the computed actions colDef. When not present but actions exist, + // we will append the actions column after validation below. + const actionsIndex = r.findIndex((c) => { + return c?._isActionsColumn === true + }) + if (onDelete) { r.push({ field: 'delete', @@ -189,16 +205,41 @@ export function transformColumnDefs({ } if (actions?.length) { - r.push({ - headerName: customActionsHeader ?? 'Actions', + const computedActionsCol = { + field: '__actions__', + _isActionsColumn: true, + headerName: 'Actions', cellRenderer: tableActionsFactory, autoHeight: true, cellStyle: { textAlign: 'center' }, cellClass: 'grid-cell-centered', - lockPosition: 'right', + // Only lock position to right if user hasn't explicitly positioned the actions column + ...(actionsIndex === -1 ? { lockPosition: 'right' } : {}), + // Set default minWidth based on number of actions (if not wrapping) + ...(!wrapActions ? { minWidth: 130 * actions?.length } : {}), + // Respect user-specified overrides when placeholder present (these should override defaults) + ...( + actionsIndex > -1 + ? { + // keep width/pin/flex/align/hide from placeholder when provided + ...(['width', 'minWidth', 'maxWidth', 'flex', 'pinned', 'headerName', 'cellStyle', 'cellClass', 'autoHeight', 'hide'] + .reduce((acc, key) => { + if (r[actionsIndex] && r[actionsIndex][key] !== undefined) acc[key] = r[actionsIndex][key] + return acc + }, {} as any)) + } + : {} + ), + ...(customActionsHeader?.trim() ? { headerName: customActionsHeader } : {}) + } - ...(!wrapActions ? { minWidth: 130 * actions?.length } : {}) - }) + if (actionsIndex > -1) { + // Replace the placeholder with computed column + r.splice(actionsIndex, 1, computedActionsCol) + } else { + // Backward compatible: append if not explicitly placed + r.push(computedActionsCol) + } } return r.map((fields) => { @@ -210,7 +251,7 @@ export function transformColumnDefs({ }) } -export function validateColumnDefs(columnDefs: ColumnDef[]): { +export function validateColumnDefs(columnDefs: WindmillColumnDef[]): { isValid: boolean errors: string[] } { diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte index 1ec82e78f3..36e8b2d84a 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ArrayStaticInputEditor.svelte @@ -3,7 +3,7 @@ import { Button } from '$lib/components/common' import { GripVertical, Loader2, Plus, X } from 'lucide-svelte' - import { createEventDispatcher } from 'svelte' + import { createEventDispatcher, getContext, tick } from 'svelte' import type { InputType, StaticInput, StaticOptions } from '../../inputType' import SubTypeEditor from './SubTypeEditor.svelte' import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action' @@ -11,6 +11,8 @@ import Toggle from '$lib/components/Toggle.svelte' import QuickAddColumn from './QuickAddColumn.svelte' import RefreshDatabaseStudioTable from './RefreshDatabaseStudioTable.svelte' + import { findGridItem } from '$lib/components/apps/editor/appUtils' + import type { AppViewerContext } from '$lib/components/apps/types' interface Props { componentInput: StaticInput & { loading?: boolean } @@ -26,6 +28,9 @@ id }: Props = $props() + const appContext = getContext('AppViewerContext') + const { app, selectedComponent } = appContext || {} + let items: ReturnType = $state([]) items = getItems(componentInput) @@ -185,10 +190,34 @@ items = getItems(componentInput) } - function deleteElementByType(index: number) { + async function updateConfiguration() { + if (selectedComponent && id) { + $selectedComponent = undefined + await tick() + $selectedComponent = [id] + } + } + + async function deleteElementByType(index: number) { if (componentInput.value) { + const item = componentInput.value[index] + // If deleting actions column, clear all table actions + if (subFieldType === 'ag-grid' && item && item._isActionsColumn === true) { + const gridItem = id ? findGridItem($app, id) : null + if (gridItem && ( + gridItem.data.type === 'aggridcomponent' || + gridItem.data.type === 'aggridcomponentee' || + gridItem.data.type === 'aggridinfinitecomponent' || + gridItem.data.type === 'aggridinfinitecomponentee' + ) && Array.isArray(gridItem.data.actions)) { + gridItem.data.actions.length = 0 + } + await updateConfiguration() + return + } + componentInput.value.splice(index, 1) - items.splice(index, 1) // Add this + items.splice(index, 1) items = items componentInput.value = componentInput.value dispatch('deleteArrayItem', { index }) diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte index bc3ae69f10..82b3e5a985 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte @@ -299,11 +299,19 @@ {:else if fieldType === 'ag-grid'}
- + {#if componentInput.value._isActionsColumn === true} +
+ Actions Column +
+ {:else} + + {/if}
{#snippet trigger()} diff --git a/frontend/src/lib/components/wizards/AgGridWizard.svelte b/frontend/src/lib/components/wizards/AgGridWizard.svelte index f799e43540..c419a0ee24 100644 --- a/frontend/src/lib/components/wizards/AgGridWizard.svelte +++ b/frontend/src/lib/components/wizards/AgGridWizard.svelte @@ -35,6 +35,8 @@ } let { value = $bindable(), trigger: trigger_render }: Props = $props() + + const isActionsColumn = $derived((value as any)?._isActionsColumn === true) const presets = [ { @@ -123,16 +125,18 @@ - + {#if !isActionsColumn} + + {/if} -
{/if}