diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonListManager.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonListManager.jsx new file mode 100644 index 0000000000..7ed9a8aaf5 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonListManager.jsx @@ -0,0 +1,118 @@ +import React, { useMemo } from 'react'; +import { SortableTree } from '@/_ui/SortableTree'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton'; +import { Button } from '@/components/ui/Button/Button'; + +const PROPERTY_NAMES = { + isGroup: 'isGroup', + parentId: 'parentId', +}; + +const ButtonListItem = ({ item, onSelect }) => ( +
{ + e.preventDefault(); + onSelect(item.id); + }} + > +
+
+ +
+ {item.buttonLabel || 'Button'} +
+
+); + +const ButtonListItemGhost = ({ item, darkMode }) => ( +
+
+
+
+ +
+ {item.buttonLabel || 'Button'} +
+
+
+); + +export const ButtonListManager = ({ buttons = [], onAddButton, onReorderButtons, onSelectButton }) => { + const items = useMemo( + () => + buttons.map((btn) => ({ + ...btn, + isGroup: false, + parentId: null, + })), + [buttons] + ); + + const handleReorder = (reorderedTree) => { + const reordered = reorderedTree.map(({ isGroup, parentId, children, ...btn }) => btn); + onReorderButtons(reordered); + }; + + const renderItem = (item) => ; + + const renderGhost = (item, { darkMode } = {}) => ; + + return ( +
+
+ + Buttons + +
+ {items.length === 0 && ( +
+
+
+ + No action button added + + + Add action buttons to table rows and configure events like you would with any button component + +
+ )} + + {items.length > 0 && ( + + )} + + + Add new action button + +
+ ); +}; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonPropertiesTab.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonPropertiesTab.jsx new file mode 100644 index 0000000000..024e7ad2f1 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonPropertiesTab.jsx @@ -0,0 +1,135 @@ +import React from 'react'; +import CodeHinter from '@/AppBuilder/CodeEditor'; +import { EventManager } from '@/AppBuilder/RightSideBar/Inspector/EventManager'; +import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties'; +import Accordion from '@/_ui/Accordion'; + +export const ButtonPropertiesTab = ({ + button, + column, + index, + darkMode, + currentState, + onButtonPropertyChange, + setColumnPopoverRootCloseBlocker, + component, + props, + columnEventChanged, + handleEventManagerPopoverCallback, +}) => { + if (!button) return null; + + const compoundRef = `${column.key || column.name}::${button.id}`; + + return ( + <> +
+ + onButtonPropertyChange('buttonLabel', value)} + componentName={`table_column_button_${button.id}_buttonLabel`} + popOverCallback={(showing) => setColumnPopoverRootCloseBlocker('buttonLabel', showing)} + /> +
+ +
+ + onButtonPropertyChange('buttonTooltip', value)} + componentName={`table_column_button_${button.id}_buttonTooltip`} + popOverCallback={(showing) => setColumnPopoverRootCloseBlocker('buttonTooltip', showing)} + /> +
+ +
+
+ onButtonPropertyChange(prop, val)} + property="loadingState" + props={button} + component={component} + paramMeta={{ type: 'toggle', displayName: 'Loading state' }} + paramType="properties" + /> +
+
+ +
+
+ onButtonPropertyChange(prop, val)} + property="buttonVisibility" + props={button} + component={component} + paramMeta={{ type: 'toggle', displayName: 'Visibility' }} + paramType="properties" + /> +
+
+ +
+
+ onButtonPropertyChange(prop, val)} + property="disableButton" + props={button} + component={component} + paramMeta={{ type: 'toggle', displayName: 'Disable action button' }} + paramType="properties" + /> +
+
+ +
+ columnEventChanged(column, events)} + apps={props.apps} + popOverCallback={(showing) => handleEventManagerPopoverCallback(showing)} + pages={props.pages} + /> + ), + }, + ]} + /> +
+ + ); +}; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonStylesTab.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonStylesTab.jsx new file mode 100644 index 0000000000..fe8d9acf7c --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ButtonStylesTab.jsx @@ -0,0 +1,182 @@ +import React, { useState } from 'react'; +import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup'; +import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem'; +import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties'; +import { Icon as IconPicker } from '@/AppBuilder/CodeBuilder/Elements/Icon'; +import AlignLeftinspector from '@/_ui/Icon/solidIcons/AlignLeftinspector'; +import AlignRightinspector from '@/_ui/Icon/solidIcons/AlignRightinspector'; + +export const ButtonStylesTab = ({ + button, + index, + darkMode, + currentState, + onButtonPropertyChange, + onButtonPropertiesChange, + component, +}) => { + const [buttonType, setButtonType] = useState(button?.buttonType || 'solid'); + const isSolid = buttonType === 'solid'; + + // Label colors considered "default" that should be swapped on mode change + const DEFAULT_LABEL = [ + '#FFFFFF', + '#ffffff', + 'var(--cc-surface1-surface)', + 'var(--text-on-solid)', + 'var(--cc-primary-text)', + ]; + + const handleTypeChange = (value) => { + setButtonType(value); + + // Batch buttonType + label color change in a single update to avoid stale closure + const updates = { buttonType: value }; + if (!button?.buttonLabelColor || DEFAULT_LABEL.includes(button.buttonLabelColor)) { + updates.buttonLabelColor = value === 'outline' ? 'var(--cc-primary-text)' : '#FFFFFF'; + } + onButtonPropertiesChange(updates); + }; + + if (!button) return null; + + return ( +
+ {/* Button type - Solid/Outline */} +
+ + + Solid + Outline + +
+ + {/* Background color - only shown in Solid mode (matches Button widget behavior) */} + {isSolid && ( +
+ onButtonPropertyChange(prop, val)} + property="buttonBackgroundColor" + props={button} + component={component} + paramMeta={{ type: 'colorSwatches', displayName: 'Background' }} + paramType="properties" + /> +
+ )} + + {/* Label color */} +
+ onButtonPropertyChange(prop, val)} + property="buttonLabelColor" + props={button} + component={component} + paramMeta={{ type: 'colorSwatches', displayName: 'Label color' }} + paramType="properties" + /> +
+ + {/* Border color */} +
+ onButtonPropertyChange(prop, val)} + property="buttonBorderColor" + props={button} + component={component} + paramMeta={{ type: 'colorSwatches', displayName: 'Border color' }} + paramType="properties" + /> +
+ + {/* Loader color */} +
+ onButtonPropertyChange(prop, val)} + property="buttonLoaderColor" + props={button} + component={component} + paramMeta={{ type: 'colorSwatches', displayName: 'Loader color' }} + paramType="properties" + /> +
+ + {/* Icon - picker with visibility toggle */} +
+ + onButtonPropertyChange('buttonIconName', value)} + onVisibilityChange={(value) => onButtonPropertyChange('buttonIconVisibility', value)} + styleDefinition={{ iconVisibility: { value: button?.buttonIconVisibility ?? false } }} + component={component} + isVisibilityEnabled={true} + /> +
+ + {/* Icon color */} +
+ onButtonPropertyChange(prop, val)} + property="buttonIconColor" + props={button} + component={component} + paramMeta={{ type: 'colorSwatches', displayName: '', showLabel: false }} + paramType="properties" + /> +
+ + {/* Icon alignment */} +
+ + onButtonPropertyChange('buttonIconAlignment', _value)} + defaultValue={button?.buttonIconAlignment || 'left'} + > + + + + + + + +
+ + {/* Border radius */} +
+ onButtonPropertyChange(prop, val)} + property="buttonBorderRadius" + props={button} + component={component} + paramMeta={{ type: 'numberInput', displayName: 'Border radius' }} + paramType="properties" + /> +
+
+ ); +}; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ColumnPopover.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ColumnPopover.jsx index 748d672d95..5964c210d1 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ColumnPopover.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/ColumnPopover.jsx @@ -3,6 +3,8 @@ import Popover from 'react-bootstrap/Popover'; import { StylesTabElements } from './StylesTabElements'; import { PropertiesTabElements } from './PropertiesTabElements'; import { TableColumnContext } from './TableColumnContext'; +import { Button } from '@/components/ui/Button/Button'; +import { useButtonManager } from '../hooks/useButtonManager'; export const ColumnPopoverContent = ({ column, @@ -16,8 +18,11 @@ export const ColumnPopoverContent = ({ props, columnEventChanged, handleEventManagerPopoverCallback, + onDuplicateColumn, + onDeleteColumn, }) => { const [activeTab, setActiveTab] = useState('propertiesTab'); + const [selectedButtonId, setSelectedButtonId] = useState(null); const [isGoingBelowScreen, setIsGoingBelowScreen] = useState(false); const popoverRef = useRef(null); @@ -68,10 +73,7 @@ export const ColumnPopoverContent = ({ } }; - // Check position after a short delay to ensure popover is rendered const timeoutId = setTimeout(checkPopoverPosition, 100); - - // Also check on window resize window.addEventListener('resize', checkPopoverPosition); return () => { @@ -80,6 +82,29 @@ export const ColumnPopoverContent = ({ }; }, [index]); + const isButtonColumn = column.columnType === 'button'; + const isButtonDetailView = isButtonColumn && selectedButtonId !== null; + + const buttonManager = useButtonManager({ column, index, onColumnItemChange }); + const { removeButton, duplicateButton } = buttonManager; + + const handleDelete = () => { + if (isButtonDetailView) { + removeButton(selectedButtonId); + setSelectedButtonId(null); + } else { + onDeleteColumn?.(); + } + }; + + const handleDuplicate = () => { + if (isButtonDetailView) { + duplicateButton(selectedButtonId); + } else { + onDuplicateColumn?.(); + } + }; + { /* TableColumnContext provides the table's component ID to all nested CodeHinters, enabling rowData/cellValue autocomplete hints in column editors (properties, styles, etc.). @@ -87,7 +112,48 @@ export const ColumnPopoverContent = ({ } return ( - + +
+
+ {isButtonDetailView && ( +
+ +
+
+
+
) : ( )} diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx index ac94b28e47..c712157bb6 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx @@ -20,6 +20,8 @@ import Check from '@/_ui/Icon/solidIcons/Check'; import Icon from '@/_ui/Icon/solidIcons/index'; import RatingIconToggle from './RatingColumn/RatingIconToggle'; import RatingColumnProperties from './RatingColumn/RatingColumnProperties'; +import { ButtonListManager } from './ButtonListManager'; +import { ButtonPropertiesTab } from './ButtonPropertiesTab'; const CustomOption = (props) => { const ColumnIcon = getColumnIcon(props.data.value); @@ -74,8 +76,12 @@ export const PropertiesTabElements = ({ columnEventChanged, timeZoneOptions, handleEventManagerPopoverCallback, + selectedButtonId, + setSelectedButtonId, + buttonManager, }) => { const { t } = useTranslation(); + const { addButton, removeButton, updateButtonProperty, reorderButtons, getButton } = buttonManager; const customStylesForSelect = { ...defaultStyles(darkMode, '100%'), @@ -83,102 +89,109 @@ export const PropertiesTabElements = ({ return ( <> - {column.columnType && } -
e.stopPropagation()}> - + {!selectedButtonId && ( + <> + {column.columnType && } +
e.stopPropagation()}> + - { - onColumnItemChange(index, 'columnType', value); - }} - value={column.columnType} - useCustomStyles={true} - styles={customStylesForSelect} - className={`column-type-table-inspector`} - /> -
-
- - onColumnItemChange(index, 'name', value)} - componentName={getPopoverFieldSource(column.columnType, 'name')} - popOverCallback={(showing) => { - setColumnPopoverRootCloseBlocker('name', showing); - }} - /> -
-
- - onColumnItemChange(index, 'key', value)} - componentName={getPopoverFieldSource(column.columnType, 'key')} - popOverCallback={(showing) => { - setColumnPopoverRootCloseBlocker('tableKey', showing); - }} - /> -
-
- - onColumnItemChange(index, 'transformation', value)} - componentName={getPopoverFieldSource(column.columnType, 'transformation')} - popOverCallback={(showing) => { - setColumnPopoverRootCloseBlocker('transformation', showing); - }} - enablePreview={false} - /> -
+ { + onColumnItemChange(index, 'columnType', value); + }} + value={column.columnType} + useCustomStyles={true} + styles={customStylesForSelect} + className={`column-type-table-inspector`} + /> +
+
+ + onColumnItemChange(index, 'name', value)} + componentName={getPopoverFieldSource(column.columnType, 'name')} + popOverCallback={(showing) => { + setColumnPopoverRootCloseBlocker('name', showing); + }} + /> +
+
+ + onColumnItemChange(index, 'key', value)} + componentName={getPopoverFieldSource(column.columnType, 'key')} + popOverCallback={(showing) => { + setColumnPopoverRootCloseBlocker('tableKey', showing); + }} + /> +
+ + )} + {column.columnType !== 'button' && ( +
+ + onColumnItemChange(index, 'transformation', value)} + componentName={getPopoverFieldSource(column.columnType, 'transformation')} + popOverCallback={(showing) => { + setColumnPopoverRootCloseBlocker('transformation', showing); + }} + enablePreview={false} + /> +
+ )} {column.columnType === 'rating' && (
@@ -193,6 +206,7 @@ export const PropertiesTabElements = ({
)} + {column.columnType === 'button' && !selectedButtonId && ( + <> +
+
+ +
+
+ + + )} + {column.columnType === 'button' && selectedButtonId && ( + updateButtonProperty(selectedButtonId, property, value)} + setColumnPopoverRootCloseBlocker={setColumnPopoverRootCloseBlocker} + component={component} + props={props} + columnEventChanged={columnEventChanged} + handleEventManagerPopoverCallback={handleEventManagerPopoverCallback} + /> + )} {(column.columnType === 'dropdown' || column.columnType === 'multiselect' || column.columnType === 'badge' || @@ -302,7 +357,7 @@ export const PropertiesTabElements = ({ />
)} - {!['image', 'link'].includes(column.columnType) && ( + {!['image', 'link', 'button'].includes(column.columnType) && (
)} -
-
- + {column.columnType !== 'button' && ( +
+
+ +
-
+ )} {['select', 'newMultiSelect', 'datepicker', 'rating'].includes(column.columnType) &&
} {column.columnType === 'datepicker' && ( diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx index 337dea9ef6..b21f8729da 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/StylesTabElements.jsx @@ -9,6 +9,7 @@ import AlignCenter from '@/_ui/Icon/solidIcons/AlignCenter'; import AlignRight from '@/_ui/Icon/solidIcons/AlignRight'; import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties'; import { Select } from '@/AppBuilder/CodeBuilder/Elements/Select'; +import { ButtonStylesTab } from './ButtonStylesTab'; export const StylesTabElements = ({ column, @@ -18,32 +19,37 @@ export const StylesTabElements = ({ onColumnItemChange, getPopoverFieldSource, component, + selectedButtonId, + buttonManager, }) => { + const { updateButtonProperty, updateButtonProperties, getButton } = buttonManager; const { t } = useTranslation(); return ( <> -
- - onColumnItemChange(index, 'horizontalAlignment', _value)} - defaultValue={column?.horizontalAlignment || 'left'} - style={{ width: '58%' }} - > - - - - - - - - - - -
+ {column.columnType !== 'button' && ( +
+ + onColumnItemChange(index, 'horizontalAlignment', _value)} + defaultValue={column?.horizontalAlignment || 'left'} + style={{ width: '58%' }} + > + + + + + + + + + + +
+ )} {column.columnType === 'toggle' && (
@@ -253,6 +259,37 @@ export const StylesTabElements = ({
)} + + {column.columnType === 'button' && !selectedButtonId && ( +
+
+ +
+
+ )} + + {column.columnType === 'button' && selectedButtonId && ( + updateButtonProperty(selectedButtonId, property, value)} + onButtonPropertiesChange={(updates) => updateButtonProperties(selectedButtonId, updates)} + component={component} + /> + )} ); }; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx index 55ce2d995d..57ff29bec8 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ProgramaticallyHandleProperties.jsx @@ -11,62 +11,10 @@ export const ProgramaticallyHandleProperties = ({ paramMeta, }) => { const getValueBasedOnProperty = (property, props) => { - switch (property) { - case 'isEditable': - return props.isEditable; - case 'disableActionButton': - return props.disableActionButton; - case 'columnVisibility': - return props.columnVisibility; - case 'fieldVisibility': - return props.fieldVisibility; - case 'linkTarget': - return props.linkTarget; - case 'isAllColumnsEditable': - return props?.isAllColumnsEditable; - case 'isAllFieldsEditable': - return props?.isAllFieldsEditable; - case 'underlineColor': - return props.underlineColor; - case 'linkColor': - return props.linkColor; - case 'useDynamicOptions': - return props?.useDynamicOptions; - case 'autoAssignColors': - return props?.autoAssignColors; - case 'makeDefaultOption': - return props?.[index]?.makeDefaultOption; - case 'textColor': - return props?.textColor; - case 'cellBackgroundColor': - return props?.cellBackgroundColor; - case 'optionsLoadingState': - return props?.optionsLoadingState; - case 'isTimeChecked': - return props?.isTimeChecked; - case 'isTwentyFourHrFormatEnabled': - return props?.isTwentyFourHrFormatEnabled; - case 'parseInUnixTimestamp': - return props?.parseInUnixTimestamp; - case 'isDateSelectionEnabled': - return props?.isDateSelectionEnabled; - case 'jsonIndentation': - return props?.jsonIndentation; - case 'labelColor': - return props?.labelColor; - case 'optionColor': - return props?.optionColor; - case 'allowHalfStar': - return props?.allowHalfStar; - case 'selectedBgColorStars': - return props?.selectedBgColorStars; - case 'selectedBgColorHearts': - return props?.selectedBgColorHearts; - case 'unselectedBgColor': - return props?.unselectedBgColor; - default: - return; + if (property === 'makeDefaultOption') { + return props?.[index]?.makeDefaultOption; } + return props?.[property]; }; const getInitialValue = (property, definitionObj) => { @@ -84,7 +32,7 @@ export const ProgramaticallyHandleProperties = ({ return value || '{{true}}'; } if (property === 'cellBackgroundColor') { - return definitionObj?.value ?? ''; + return definitionObj?.value || 'var(--cc-surface1-surface)'; } if (property === 'textColor') { return definitionObj?.value ?? '#11181C'; @@ -109,6 +57,33 @@ export const ProgramaticallyHandleProperties = ({ if (property === 'jsonIndentation') { return definitionObj?.value ?? `{{true}}`; } + if (property === 'buttonVisibility') { + return definitionObj?.value ?? '{{true}}'; + } + if (property === 'disableButton') { + return definitionObj?.value ?? '{{false}}'; + } + if (property === 'loadingState') { + return definitionObj?.value ?? '{{false}}'; + } + if (property === 'buttonBackgroundColor') { + return definitionObj?.value ?? 'var(--cc-primary-brand)'; + } + if (property === 'buttonLabelColor') { + return definitionObj?.value ?? 'var(--cc-surface1-surface)'; + } + if (property === 'buttonIconColor') { + return definitionObj?.value ?? 'var(--cc-surface1-surface)'; + } + if (property === 'buttonLoaderColor') { + return definitionObj?.value ?? 'var(--cc-surface1-surface)'; + } + if (property === 'buttonBorderColor') { + return definitionObj?.value ?? 'var(--cc-weak-border)'; + } + if (property === 'buttonBorderRadius') { + return definitionObj?.value ?? '6'; + } return definitionObj?.value ?? `{{false}}`; }; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx index f6dd02abe7..1b6586dcf6 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/Table.jsx @@ -16,6 +16,8 @@ import NoListItem from './NoListItem'; import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties'; import { ColumnPopoverContent } from './ColumnManager/ColumnPopover'; import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg'; +import { ToolTip } from '@/_components/ToolTip'; +import Icon from '@/_ui/Icon/solidIcons/index'; import { ColorSwatches } from '@/modules/Appbuilder/components'; import { getColumnIcon } from './utils'; import { getSafeRenderableValue } from '@/AppBuilder/Widgets/utils'; @@ -45,6 +47,7 @@ const getColumnTypeDisplayText = (columnType) => { json: 'JSON', markdown: 'Markdown', html: 'HTML', + button: 'Button', }; return displayMap[columnType] ?? capitalize(columnType ?? ''); }; @@ -153,6 +156,8 @@ export const Table = (props) => { props={props} columnEventChanged={handleColumnEventChange} handleEventManagerPopoverCallback={handleEventManagerPopoverCallback} + onDuplicateColumn={() => duplicateColumn(index)} + onDeleteColumn={() => removeColumn(index, `${column.name}-${index}`)} /> ), @@ -166,6 +171,8 @@ export const Table = (props) => { props, handleColumnEventChange, handleEventManagerPopoverCallback, + duplicateColumn, + removeColumn, ] ); @@ -488,6 +495,7 @@ export const Table = (props) => { handleToggleColumnPopover(index, show)} @@ -554,9 +562,27 @@ export const Table = (props) => {
), }, - // Action buttons section + // Action buttons section (deprecated — replaced by button column type) { - title: 'Action buttons', + title: ( +
+ Action buttons + + These Action buttons are deprecated and will be removed in a future update. Use the new Button column + instead by adding a new column and selecting type as a button. +
+ } + show={true} + placement="bottom" + > + + + + +
+ ), children: (
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 = ( + + ); + + 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);