diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/_assets/ButtonTypeIcon.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/_assets/ButtonTypeIcon.jsx
new file mode 100644
index 0000000000..a35887ea37
--- /dev/null
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/_assets/ButtonTypeIcon.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+const ButtonTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
+
+
+
+
+);
+
+export default ButtonTypeIcon;
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/_assets/index.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/_assets/index.js
index 0e97ea8675..9ebe274952 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/_assets/index.js
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/_assets/index.js
@@ -14,3 +14,4 @@ export { default as BadgeTypeIcon } from './BadgeTypeIcon';
export { default as TagsTypeIcon } from './TagsTypeIcon';
export { default as RadioTypeIcon } from './RadioTypeIcon';
export { default as RatingTypeIcon } from './RadioTypeIcon';
+export { default as ButtonTypeIcon } from './ButtonTypeIcon';
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/hooks/useButtonManager.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/hooks/useButtonManager.js
new file mode 100644
index 0000000000..b7fc28ecf4
--- /dev/null
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/hooks/useButtonManager.js
@@ -0,0 +1,72 @@
+import { v4 as uuidv4 } from 'uuid';
+import useStore from '@/AppBuilder/_stores/store';
+
+export const DEFAULT_BUTTON = {
+ buttonLabel: 'Button',
+ buttonTooltip: '',
+ disableButton: false,
+ loadingState: false,
+ buttonVisibility: true,
+ buttonType: 'solid',
+ buttonBackgroundColor: 'var(--cc-primary-brand)',
+ buttonLabelColor: '#FFFFFF',
+ buttonBorderColor: 'var(--cc-primary-brand)',
+ buttonBorderRadius: '6',
+ buttonLoaderColor: 'var(--cc-surface1-surface)',
+ buttonIconName: 'IconHome2',
+ buttonIconVisibility: false,
+ buttonIconColor: 'var(--cc-default-icon)',
+ buttonIconAlignment: 'left',
+};
+
+export const useButtonManager = ({ column, index, onColumnItemChange }) => {
+ const addButton = () => {
+ const newButton = { ...DEFAULT_BUTTON, id: uuidv4() };
+ const updatedButtons = [...(column.buttons || []), newButton];
+ onColumnItemChange(index, 'buttons', updatedButtons);
+ return newButton.id;
+ };
+
+ const removeButton = (buttonId) => {
+ const updatedButtons = (column.buttons || []).filter((b) => b.id !== buttonId);
+ onColumnItemChange(index, 'buttons', updatedButtons);
+
+ // Clean up events for this button
+ const columnKey = column.key || column.name;
+ const ref = `${columnKey}::${buttonId}`;
+ const { getModuleEvents, deleteAppVersionEventHandler } = useStore.getState().eventsSlice;
+ const events = getModuleEvents('canvas').filter((e) => e.target === 'table_column' && e.event?.ref === ref);
+ Promise.all(events.map((e) => deleteAppVersionEventHandler(e.id))).catch((err) => {
+ console.error('[useButtonManager] Failed to delete event handlers for button', buttonId, err);
+ });
+ };
+
+ const updateButtonProperty = (buttonId, property, value) => {
+ const updatedButtons = (column.buttons || []).map((b) => (b.id === buttonId ? { ...b, [property]: value } : b));
+ onColumnItemChange(index, 'buttons', updatedButtons);
+ };
+
+ const updateButtonProperties = (buttonId, updates) => {
+ const updatedButtons = (column.buttons || []).map((b) => (b.id === buttonId ? { ...b, ...updates } : b));
+ onColumnItemChange(index, 'buttons', updatedButtons);
+ };
+
+ const reorderButtons = (reorderedButtons) => {
+ onColumnItemChange(index, 'buttons', reorderedButtons);
+ };
+
+ const duplicateButton = (buttonId) => {
+ const button = (column.buttons || []).find((b) => b.id === buttonId);
+ if (!button) return null;
+ const newButton = { ...button, id: uuidv4() };
+ const updatedButtons = [...(column.buttons || []), newButton];
+ onColumnItemChange(index, 'buttons', updatedButtons);
+ return newButton.id;
+ };
+
+ const getButton = (buttonId) => {
+ return (column.buttons || []).find((b) => b.id === buttonId);
+ };
+
+ return { addButton, removeButton, duplicateButton, updateButtonProperty, updateButtonProperties, reorderButtons, getButton };
+};
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/hooks/useColumnManager.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/hooks/useColumnManager.js
index 456fc55f77..6e08a007d6 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/hooks/useColumnManager.js
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/hooks/useColumnManager.js
@@ -43,6 +43,16 @@ export const useColumnManager = ({ component, paramUpdated, currentState }) => {
};
}
+ // Handle button column initialization — starts with empty buttons array
+ if (property === 'columnType' && value === 'button') {
+ modifiedColumn = {
+ ...modifiedColumn,
+ columnVisibility: true,
+ horizontalAlignment: 'left',
+ buttons: [],
+ };
+ }
+
return modifiedColumn;
}, []);
@@ -62,6 +72,16 @@ export const useColumnManager = ({ component, paramUpdated, currentState }) => {
if (ref) {
await deleteEvents({ ref }, 'table_column');
}
+
+ // Clean up events for all buttons in removed button columns
+ for (const col of removedColumns) {
+ if (col.columnType === 'button' && col.buttons) {
+ const columnKey = col.key || col.name;
+ for (const btn of col.buttons) {
+ await deleteEvents({ ref: `${columnKey}::${btn.id}` }, 'table_column');
+ }
+ }
+ }
},
[component, paramUpdated, deleteEvents]
);
@@ -75,6 +95,9 @@ export const useColumnManager = ({ component, paramUpdated, currentState }) => {
typeProp: 'columnType',
nonEditableTypes: ['link', 'image'],
namePrefix: 'new_column',
+ defaultItemProps: {
+ includeKey: true,
+ },
onPropertyChange: handlePropertyChange,
onRemove: handleRemove,
},
diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/utils.js b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/utils.js
index 34bef09266..32fb329002 100644
--- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/utils.js
+++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/utils.js
@@ -15,6 +15,7 @@ import {
TagsTypeIcon,
RadioTypeIcon,
RatingTypeIcon,
+ ButtonTypeIcon,
} from './_assets';
export const getColumnIcon = (columnType) => {
@@ -57,6 +58,8 @@ export const getColumnIcon = (columnType) => {
return TagsTypeIcon;
case 'rating':
return RatingTypeIcon;
+ case 'button':
+ return ButtonTypeIcon;
default:
return null;
}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/adapters/ButtonColumnAdapter.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/adapters/ButtonColumnAdapter.jsx
new file mode 100644
index 0000000000..b854255a71
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/adapters/ButtonColumnAdapter.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import { Button } from '@/components/ui/Button/Button';
+import TablerIcon from '@/_ui/Icon/TablerIcon';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import Tooltip from 'react-bootstrap/Tooltip';
+
+export const ButtonColumn = ({
+ buttonLabel,
+ buttonType,
+ disableButton,
+ loadingState,
+ backgroundColor,
+ labelColor,
+ iconName,
+ iconVisibility,
+ iconColor,
+ iconAlignment,
+ loaderColor,
+ borderColor,
+ borderRadius,
+ tooltip,
+ onClick,
+}) => {
+ const handleClick = (e) => {
+ e.stopPropagation();
+ if (onClick) onClick();
+ };
+
+ const variant = buttonType === 'outline' ? 'outline' : 'primary';
+ const isOutline = variant === 'outline';
+
+ // Compute colors based on solid/outline mode, matching Button widget behavior.
+ // When colors are at defaults, adapt them to the current mode.
+ // When user has customized, keep the custom value.
+ const DEFAULT_LABEL_COLORS = ['#FFFFFF', '#ffffff', 'var(--cc-surface1-surface)'];
+ const DEFAULT_BG_COLORS = ['#4368E3', '#4368e3', 'var(--cc-primary-brand)'];
+ const DEFAULT_BORDER_COLORS = [...DEFAULT_BG_COLORS, 'var(--cc-weak-border)'];
+ const DEFAULT_ICON_COLORS = [
+ 'var(--cc-default-icon)',
+ 'var(--cc-default-icon)',
+ 'var(--cc-surface1-surface)',
+ '#FFFFFF',
+ '#ffffff',
+ ];
+ const DEFAULT_LOADER_COLORS = ['#FFFFFF', '#ffffff', 'var(--cc-surface1-surface)'];
+
+ const isDefaultLabel = !labelColor || DEFAULT_LABEL_COLORS.includes(labelColor);
+ const isDefaultIcon = !iconColor || DEFAULT_ICON_COLORS.includes(iconColor);
+ const isDefaultBg = !backgroundColor || DEFAULT_BG_COLORS.includes(backgroundColor);
+ const isDefaultBorder = !borderColor || DEFAULT_BORDER_COLORS.includes(borderColor);
+ const isDefaultLoader = !loaderColor || DEFAULT_LOADER_COLORS.includes(loaderColor);
+
+ const computedBgColor = isDefaultBg
+ ? isOutline
+ ? 'transparent'
+ : 'var(--cc-primary-brand)'
+ : isOutline
+ ? 'transparent'
+ : backgroundColor;
+
+ const computedLabelColor = isDefaultLabel
+ ? isOutline
+ ? 'var(--cc-primary-text)'
+ : 'var(--text-on-solid)'
+ : labelColor;
+
+ const computedIconColor = isDefaultIcon ? (isOutline ? 'var(--cc-default-icon)' : 'var(--icon-on-solid)') : iconColor;
+
+ const computedBorderColor = isDefaultBorder ? (isOutline ? 'var(--borders-strong)' : undefined) : borderColor;
+
+ const computedLoaderColor = isDefaultLoader ? (isOutline ? 'var(--cc-primary-brand)' : '#FFFFFF') : loaderColor;
+
+ let iconElement = null;
+ if (iconName && iconVisibility) {
+ iconElement =
;
+ }
+
+ const buttonStyle = {
+ padding: '4px 10px',
+ borderRadius: borderRadius ? `${borderRadius}px` : '6px',
+ fontSize: '12px',
+ fontWeight: 500,
+ lineHeight: '20px',
+ gap: '6px',
+ height: '28px',
+ backgroundColor: computedBgColor,
+ color: computedLabelColor,
+ };
+ if (computedBorderColor) {
+ buttonStyle.borderColor = computedBorderColor;
+ buttonStyle.borderStyle = 'solid';
+ buttonStyle.borderWidth = '1px';
+ }
+ if (disableButton) {
+ buttonStyle.opacity = '50%';
+ }
+
+ const buttonElement = (
+
+ {iconAlignment === 'left' && iconElement}
+ {buttonLabel || 'Button'}
+ {iconAlignment === 'right' && iconElement}
+
+ );
+
+ const hasTooltip = tooltip && tooltip.toString().trim();
+
+ if (hasTooltip) {
+ return (
+
{tooltip}}>
+ {buttonElement}
+
+ );
+ }
+
+ return buttonElement;
+};
+
+export default ButtonColumn;
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/adapters/ButtonColumnGroupAdapter.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/adapters/ButtonColumnGroupAdapter.jsx
new file mode 100644
index 0000000000..96406b2f8d
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/adapters/ButtonColumnGroupAdapter.jsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import useStore from '@/AppBuilder/_stores/store';
+import useTableStore from '../../../_stores/tableStore';
+import { shallow } from 'zustand/shallow';
+import { ButtonColumn } from './ButtonColumnAdapter';
+
+export const ButtonColumnGroup = ({ id, buttons = [], cellBackgroundColor, cellValue, rowData, onClick }) => {
+ const getResolvedValue = useStore((state) => state.getResolvedValue);
+ const getTableColumnEvents = useTableStore((state) => state.getTableColumnEvents, shallow);
+
+ return (
+
+ {buttons.map((button) => {
+ const context = { cellValue, rowData };
+ const resolvedVisibility = getResolvedValue(button.buttonVisibility, context);
+ if (resolvedVisibility === false) return null;
+
+ const resolvedLabel = getResolvedValue(button.buttonLabel, context) || 'Button';
+ const resolvedType = getResolvedValue(button.buttonType, context) || 'solid';
+ const resolvedDisable = getResolvedValue(button.disableButton, context);
+ const resolvedLoading = getResolvedValue(button.loadingState, context);
+ const resolvedBgColor = getResolvedValue(button.buttonBackgroundColor, context);
+ const resolvedLabelColor = getResolvedValue(button.buttonLabelColor, context);
+ const resolvedIconName = getResolvedValue(button.buttonIconName, context);
+ const resolvedIconVisibility = getResolvedValue(button.buttonIconVisibility, context);
+ const resolvedIconColor = getResolvedValue(button.buttonIconColor, context);
+ const resolvedIconAlignment = getResolvedValue(button.buttonIconAlignment, context) || 'left';
+ const resolvedLoaderColor = getResolvedValue(button.buttonLoaderColor, context);
+ const resolvedBorderColor = getResolvedValue(button.buttonBorderColor, context);
+ const resolvedBorderRadius = getResolvedValue(button.buttonBorderRadius, context);
+ const resolvedTooltip = getResolvedValue(button.buttonTooltip, context);
+
+ return (
+ {
+ if (onClick) onClick(button.id, getTableColumnEvents(id));
+ }}
+ />
+ );
+ })}
+
+ );
+};
+
+export default ButtonColumnGroup;
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/index.js b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/index.js
index 500156816e..499cd2202e 100644
--- a/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/index.js
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/DataTypes/index.js
@@ -10,6 +10,7 @@ export { CustomSelectColumn } from './adapters/SelectColumnAdapter'; // Select &
export { JsonColumn } from './adapters/JsonColumnAdapter';
export { MarkdownColumn } from './adapters/MarkdownColumnAdapter';
export { HTMLColumn } from './adapters/HtmlColumnAdapter';
+export { ButtonColumnGroup } from './adapters/ButtonColumnGroupAdapter';
// Deprecated columns not moved to shared renderers
export { ToggleColumn } from './Toggle';
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx
index 0612139ba7..e3235e0eae 100644
--- a/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_components/TableData/_components/TableRow.jsx
@@ -57,15 +57,18 @@ export const TableRow = ({
data-cy={`${generateCypressDataCy(componentName)}-row-${virtualRow.index}`}
>
{row.getVisibleCells().map((cell) => {
+ const isButtonColumn = cell.column.columnDef?.meta?.columnType === 'button';
const cellStyles = {
backgroundColor: getResolvedValue(cell.column.columnDef?.meta?.cellBackgroundColor ?? 'inherit', {
rowData: row.original,
cellValue: cell.getValue(),
}),
- justifyContent: determineJustifyContentValue(cell.column.columnDef?.meta?.horizontalAlignment),
+ justifyContent: isButtonColumn
+ ? undefined
+ : determineJustifyContentValue(cell.column.columnDef?.meta?.horizontalAlignment),
display: 'flex',
alignItems: 'center',
- textAlign: cell.column.columnDef?.meta?.horizontalAlignment,
+ textAlign: isButtonColumn ? undefined : cell.column.columnDef?.meta?.horizontalAlignment,
width: cell.column.getSize(),
};
@@ -79,10 +82,15 @@ export const TableRow = ({
return (
{flexRender(cell.column?.columnDef?.cell, cell.getContext())}
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateColumnsData.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateColumnsData.js
index ebc7660488..85f2275e67 100644
--- a/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateColumnsData.js
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/generateColumnsData.js
@@ -15,6 +15,7 @@ import {
JsonColumn,
MarkdownColumn,
HTMLColumn,
+ ButtonColumnGroup,
// Deprecated columns
TagsColumn,
RadioColumn,
@@ -23,8 +24,57 @@ import {
RatingColumn,
} from '../_components/DataTypes';
import useTableStore from '../_stores/tableStore';
+import { normalizeButtonEvent } from './normalizeButtonEvent';
import SelectSearch from 'react-select-search';
+// Module-level singleton for text measurement (avoids creating canvas on every call)
+let _measureCanvas = null;
+const getMeasureContext = () => {
+ if (!_measureCanvas) {
+ _measureCanvas = document.createElement('canvas');
+ }
+ const ctx = _measureCanvas.getContext('2d');
+ ctx.font = '500 12px "IBM Plex Sans"';
+ return ctx;
+};
+
+// Calculate width needed for button column based on button labels/icons
+const calculateButtonColumnWidth = (buttons, getResolvedValue) => {
+ if (!buttons || buttons.length === 0) return 90;
+
+ const context = getMeasureContext();
+
+ let totalWidth = 0;
+ const cellPadding = 24; // .has-actions: padding 0 12px (12px each side)
+ const buttonGap = 6; // ButtonColumnGroup: gap 6px
+
+ let visibleCount = 0;
+ buttons.forEach((button) => {
+ // Only skip when explicitly false (matching ButtonColumnGroupAdapter behavior)
+ const isVisible = getResolvedValue(button.buttonVisibility);
+ if (isVisible === false) return;
+
+ const label = getResolvedValue(button.buttonLabel) || 'Button';
+ const textWidth = context.measureText(label).width;
+
+ // Button style: padding 4px 10px = 20px horizontal, border 1px each side = 2px (conservative upper bound)
+ const buttonPadding = 20;
+ const buttonBorder = 2;
+ // Icon: 14px icon + 6px gap (button internal gap) when visible
+ const iconVisible = getResolvedValue(button.buttonIconVisibility);
+ const iconWidth = iconVisible ? 20 : 0;
+
+ totalWidth += textWidth + buttonPadding + buttonBorder + iconWidth;
+ visibleCount++;
+ });
+
+ // Add gaps between buttons
+ if (visibleCount > 1) totalWidth += buttonGap * (visibleCount - 1);
+
+ totalWidth += cellPadding;
+ return Math.max(90, Math.ceil(totalWidth));
+};
+
export default function generateColumnsData({
columnProperties,
columnSizes,
@@ -413,12 +463,63 @@ export default function generateColumnsData({
);
}
+ case 'button': {
+ if (columnForAddNewRow) return ;
+ const columnKey = column?.key || column?.name;
+ const buttons = column.buttons || [];
+
+ return (
+ {
+ const buttonEvents = tableColumnEvents.filter(
+ (event) => event?.event?.ref === `${columnKey}::${buttonId}`
+ );
+
+ // Dynamic mode: merge inline events from button data
+ const useDynamicColumn =
+ useTableStore.getState().components?.[id]?.columnDetails?.useDynamicColumn ?? false;
+ if (useDynamicColumn) {
+ const button = buttons.find((b) => b.id === buttonId);
+ const inlineEvents = (button?.events || [])
+ .map((evt) => {
+ const normalized = normalizeButtonEvent(evt, buttonId);
+ if (!normalized) return null;
+ return { event: { ...normalized, ref: `${columnKey}::${buttonId}` } };
+ })
+ .filter(Boolean);
+ buttonEvents.push(...inlineEvents);
+ }
+
+ fireEvent('OnTableButtonColumnClicked', {
+ column,
+ buttonId,
+ tableColumnEvents: buttonEvents,
+ });
+ }}
+ />
+ );
+ }
+
default:
return cellValue || '';
}
},
};
+ // Disable sorting, filtering, and resizing for button columns; auto-size to content
+ if (columnType === 'button') {
+ columnDef.enableSorting = false;
+ columnDef.enableColumnFilter = false;
+ columnDef.enableResizing = false;
+ const buttons = column.buttons || [];
+ columnDef.size = calculateButtonColumnWidth(buttons, getResolvedValue);
+ }
+
// Add sorting configuration for specific column types
if (columnType === 'number') {
columnDef.sortingFn = (rowA, rowB, columnId) => {
diff --git a/frontend/src/AppBuilder/Widgets/NewTable/_utils/normalizeButtonEvent.js b/frontend/src/AppBuilder/Widgets/NewTable/_utils/normalizeButtonEvent.js
new file mode 100644
index 0000000000..c145c1b787
--- /dev/null
+++ b/frontend/src/AppBuilder/Widgets/NewTable/_utils/normalizeButtonEvent.js
@@ -0,0 +1,64 @@
+import { ActionTypes } from '@/AppBuilder/RightSideBar/Inspector/ActionTypes';
+
+// Derives the label-to-id mapping from the canonical ActionTypes array so it stays
+// in sync automatically when actions are added or changed.
+// Supports both space-separated ("switch page") and collapsed ("switchpage") formats.
+const ACTION_LABEL_TO_ID = ActionTypes.reduce((map, action) => {
+ const lowerName = action.name.toLowerCase();
+ map[lowerName] = action.id;
+ // Also add a collapsed (no-spaces) variant for convenience
+ const collapsed = lowerName.replace(/\s+/g, '');
+ if (collapsed !== lowerName) {
+ map[collapsed] = action.id;
+ }
+ return map;
+}, {});
+
+// Maps user-friendly event labels to internal eventId values
+const EVENT_LABEL_TO_ID = {
+ 'on click': 'onClick',
+ 'on change': 'onChange',
+ 'on focus': 'onFocus',
+ 'on blur': 'onBlur',
+ 'on hover': 'onHover',
+};
+
+/**
+ * Normalizes a user-friendly button event object into the internal format expected by executeAction.
+ *
+ * User format:
+ * { event: "On click", action: "Show Alert", message: "Hello!", alertType: "success" }
+ *
+ * Internal format:
+ * { eventId: "onClick", actionId: "show-alert", message: "Hello!", alertType: "success" }
+ *
+ * If the event already uses internal keys (actionId), it is passed through unchanged.
+ */
+export function normalizeButtonEvent(evt, buttonId) {
+ // Already in internal format — pass through
+ if (evt.actionId) return evt;
+
+ const { event: eventLabel, action: actionLabel, ...rest } = evt;
+
+ // Map action label → actionId
+ const actionId = actionLabel ? ACTION_LABEL_TO_ID[actionLabel.toLowerCase()] : undefined;
+ if (actionLabel && !actionId) {
+ const available = Object.keys(ACTION_LABEL_TO_ID)
+ .map((k) => `"${k}"`)
+ .join(', ');
+ console.warn(`[Table] Unknown action "${actionLabel}" in button "${buttonId}". Available actions: ${available}`);
+ return null;
+ }
+
+ // Map event label → eventId
+ const eventId = eventLabel ? EVENT_LABEL_TO_ID[eventLabel.toLowerCase()] : 'onClick';
+ if (eventLabel && !eventId) {
+ const available = Object.keys(EVENT_LABEL_TO_ID)
+ .map((k) => `"${k}"`)
+ .join(', ');
+ console.warn(`[Table] Unknown event "${eventLabel}" in button "${buttonId}". Available events: ${available}`);
+ return null;
+ }
+
+ return { ...rest, eventId, actionId };
+}
diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
index fdad3a4513..78681bae70 100644
--- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js
@@ -326,6 +326,20 @@ export const createEventsSlice = (set, get) => ({
}
}
+ if (eventName === 'OnTableButtonColumnClicked') {
+ const { column, tableColumnEvents } = options;
+
+ if (column && tableColumnEvents) {
+ for (const event of tableColumnEvents) {
+ if (event?.event?.actionId) {
+ await get().eventsSlice.executeAction(event.event, mode, customVariables, moduleId);
+ }
+ }
+ } else {
+ console.log('No action is associated with this event');
+ }
+ }
+
if (eventName === 'onCalendarEventSelect') {
const { id, calendarEvent } = options;
setExposedValue(id, 'selectedEvent', calendarEvent);
@@ -943,12 +957,23 @@ export const createEventsSlice = (set, get) => ({
}
case 'switch-page': {
try {
- const { pageId } = event;
+ let { pageId } = event;
+ const { pageHandle } = event;
+
+ // Resolve pageHandle → pageId if pageId not provided
+ if (!pageId && pageHandle) {
+ const pages = get().modules[moduleId].pages;
+ pageId = pages.find((p) => p.handle === pageHandle.toLowerCase())?.id;
+ if (!pageId) {
+ throw new Error(`Invalid page handle: "${pageHandle}"`);
+ }
+ }
+
if (!pageId) {
- throw new Error('No page ID provided');
+ throw new Error('Either pageId or pageHandle must be provided');
}
const { switchPage } = get();
- const page = get().modules[moduleId].pages.find((page) => page.id === event.pageId);
+ const page = get().modules[moduleId].pages.find((page) => page.id === pageId);
const queryParams = event.queryParams || [];
if (page.restricted && mode !== 'edit') {
toast.error('Access to this page is restricted. Contact admin to know more.');
diff --git a/frontend/src/_styles/table-component.scss b/frontend/src/_styles/table-component.scss
index 5dd77ccb27..a2b1936e80 100644
--- a/frontend/src/_styles/table-component.scss
+++ b/frontend/src/_styles/table-component.scss
@@ -1207,6 +1207,7 @@
z-index: 999 !important;
min-width: 280px;
max-height: 85vh !important;
+ background-color: var(--cc-surface1-surface);
.popover-body {
display: flex;
@@ -1214,8 +1215,10 @@
padding: 16px 0px 16px !important;
gap: 16px !important;
align-self: stretch;
- background-color: var(--slate1) !important;
+ // background-color: var(--slate1) !important;
border-radius: 0;
+ overflow-y: auto;
+ max-height: calc(85vh - 40px);
.optional-properties-when-editable-true {
// background-color: var(--slate3);
@@ -1258,8 +1261,12 @@
}
.popover-header {
- padding: 4px 0 0 0 !important;
- background-color: var(--slate3);
+ padding: 8px 16px 0px 16px !important;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ background-color: var(--surfaces-surface-01);
+ border-bottom: 1px solid var(--borders-weak-disabled) !important;
.active-column-tab {
border-bottom: 1px solid var(--indigo9);