mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
Merge pull request #15258 from ToolJet/feat/button-column
Feat : introduce a button new column type in table widget
This commit is contained in:
commit
4e57909da8
21 changed files with 1324 additions and 207 deletions
|
|
@ -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 }) => (
|
||||
<div
|
||||
className="page-menu-item"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect(item.id);
|
||||
}}
|
||||
>
|
||||
<div className="left">
|
||||
<div className="main-page-icon-wrapper">
|
||||
<SolidIcon name="cursor" width={16} />
|
||||
</div>
|
||||
<span className="page-name">{item.buttonLabel || 'Button'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ButtonListItemGhost = ({ item, darkMode }) => (
|
||||
<div className={`nav-handler ghost ${darkMode ? 'dark-theme' : ''}`}>
|
||||
<div className="page-menu-item" style={{ width: '100%' }}>
|
||||
<div className="left">
|
||||
<div className="main-page-icon-wrapper">
|
||||
<SolidIcon name="cursor" width={16} />
|
||||
</div>
|
||||
<span className="page-name">{item.buttonLabel || 'Button'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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) => <ButtonListItem item={item} onSelect={onSelectButton} />;
|
||||
|
||||
const renderGhost = (item, { darkMode } = {}) => <ButtonListItemGhost item={item} darkMode={darkMode} />;
|
||||
|
||||
return (
|
||||
<div className="button-list-manager navigation-inspector" style={{ padding: '0 12px' }}>
|
||||
<div
|
||||
className="d-flex align-items-center justify-content-center tj-text-xsm"
|
||||
style={{ color: 'var(--text-placeholder)', gap: '8px', marginBottom: '8px' }}
|
||||
>
|
||||
<span style={{ flex: 1, borderTop: '1px dashed var(--border-weak)' }} />
|
||||
<span>Buttons</span>
|
||||
<span style={{ flex: 1, borderTop: '1px dashed var(--border-weak)' }} />
|
||||
</div>
|
||||
{items.length === 0 && (
|
||||
<div className="d-flex justify-content-center align-items-center gap-[8px] flex-column">
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--cc-surface2-surface)',
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
borderRadius: '8px',
|
||||
padding: '6px',
|
||||
display: 'inline-flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
<Button variant="ghost" size="default" iconOnly={true} isLucid={true} trailingIcon="mouse" />
|
||||
</div>
|
||||
<span className="flex tj-header-h8" style={{ color: 'var(--cc-text-default)' }}>
|
||||
No action button added
|
||||
</span>
|
||||
<span className="flex tj-text-xsm text-center" style={{ color: 'var(--cc-text-placeholder)' }}>
|
||||
Add action buttons to table rows and configure events like you would with any button component
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length > 0 && (
|
||||
<SortableTree
|
||||
items={items}
|
||||
onReorder={handleReorder}
|
||||
propertyNames={PROPERTY_NAMES}
|
||||
renderItem={renderItem}
|
||||
renderGhost={renderGhost}
|
||||
collapsible={false}
|
||||
indicator={true}
|
||||
indentationWidth={0}
|
||||
handlerClassName="button-list-handler"
|
||||
containerElement="div"
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddNewButton onClick={onAddButton} dataCy="add-new-action-button" className="mt-2 w-100">
|
||||
Add new action button
|
||||
</AddNewButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<div className="field mb-2 px-3">
|
||||
<label className="form-label">Button label</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={button.buttonLabel ?? 'Button'}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder="Button label"
|
||||
onChange={(value) => onButtonPropertyChange('buttonLabel', value)}
|
||||
componentName={`table_column_button_${button.id}_buttonLabel`}
|
||||
popOverCallback={(showing) => setColumnPopoverRootCloseBlocker('buttonLabel', showing)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field mb-2 px-3">
|
||||
<label className="form-label">Tooltip</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={button.buttonTooltip ?? ''}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder="Enter tooltip text"
|
||||
onChange={(value) => onButtonPropertyChange('buttonTooltip', value)}
|
||||
componentName={`table_column_button_${button.id}_buttonTooltip`}
|
||||
popOverCallback={(showing) => setColumnPopoverRootCloseBlocker('buttonTooltip', showing)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Loading state"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="loadingState"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Loading state' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Visibility"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="buttonVisibility"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Visibility' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Disable action button"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="disableButton"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Disable action button' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: '1px solid var(--border-weak)' }}>
|
||||
<Accordion
|
||||
items={[
|
||||
{
|
||||
title: 'Events',
|
||||
isOpen: true,
|
||||
children: (
|
||||
<EventManager
|
||||
sourceId={props?.component?.id}
|
||||
eventSourceType="table_column"
|
||||
customEventRefs={{ ref: compoundRef }}
|
||||
hideEmptyEventsAlert={false}
|
||||
eventMetaDefinition={{ events: { onClick: { displayName: 'On click' } } }}
|
||||
currentState={currentState}
|
||||
dataQueries={props.dataQueries}
|
||||
components={props.components}
|
||||
eventsChanged={(events) => columnEventChanged(column, events)}
|
||||
apps={props.apps}
|
||||
popOverCallback={(showing) => handleEventManagerPopoverCallback(showing)}
|
||||
pages={props.pages}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<div className="d-flex flex-column custom-gap-16">
|
||||
{/* Button type - Solid/Outline */}
|
||||
<div className="field d-flex align-items-center justify-content-between px-3">
|
||||
<label className="tj-text-xsm color-slate12">Button type</label>
|
||||
<ToggleGroup onValueChange={handleTypeChange} defaultValue={buttonType}>
|
||||
<ToggleGroupItem value="solid">Solid</ToggleGroupItem>
|
||||
<ToggleGroupItem value="outline">Outline</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* Background color - only shown in Solid mode (matches Button widget behavior) */}
|
||||
{isSolid && (
|
||||
<div className="field px-3">
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Background"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="buttonBackgroundColor"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'colorSwatches', displayName: 'Background' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Label color */}
|
||||
<div className="field px-3">
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Label color"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="buttonLabelColor"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'colorSwatches', displayName: 'Label color' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Border color */}
|
||||
<div className="field px-3">
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Border color"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="buttonBorderColor"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'colorSwatches', displayName: 'Border color' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Loader color */}
|
||||
<div className="field px-3">
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Loader color"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="buttonLoaderColor"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'colorSwatches', displayName: 'Loader color' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Icon - picker with visibility toggle */}
|
||||
<div className="field d-flex align-items-center justify-content-between px-3">
|
||||
<label className="tj-text-xsm color-slate12">Icon</label>
|
||||
<IconPicker
|
||||
value={button?.buttonIconName || 'IconHome2'}
|
||||
onChange={(value) => onButtonPropertyChange('buttonIconName', value)}
|
||||
onVisibilityChange={(value) => onButtonPropertyChange('buttonIconVisibility', value)}
|
||||
styleDefinition={{ iconVisibility: { value: button?.buttonIconVisibility ?? false } }}
|
||||
component={component}
|
||||
isVisibilityEnabled={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Icon color */}
|
||||
<div className="field px-3">
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Icon color"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="buttonIconColor"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'colorSwatches', displayName: '', showLabel: false }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Icon alignment */}
|
||||
<div className="field d-flex align-items-center justify-content-between px-3">
|
||||
<label className="tj-text-xsm color-slate12"></label>
|
||||
<ToggleGroup
|
||||
onValueChange={(_value) => onButtonPropertyChange('buttonIconAlignment', _value)}
|
||||
defaultValue={button?.buttonIconAlignment || 'left'}
|
||||
>
|
||||
<ToggleGroupItem value="left">
|
||||
<AlignLeftinspector width={14} fill="#C1C8CD" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="right">
|
||||
<AlignRightinspector width={14} fill="#C1C8CD" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* Border radius */}
|
||||
<div className="field px-3">
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Border radius"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={(_, prop, val) => onButtonPropertyChange(prop, val)}
|
||||
property="buttonBorderRadius"
|
||||
props={button}
|
||||
component={component}
|
||||
paramMeta={{ type: 'numberInput', displayName: 'Border radius' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<TableColumnContext.Provider value={component?.id}>
|
||||
<Popover.Header>
|
||||
<Popover.Header style={{ padding: '8px 16px 0 16px' }}>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div className="d-flex custom-gap-6 flex-row items-center justify-start">
|
||||
{isButtonDetailView && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
iconOnly={true}
|
||||
isLucid={true}
|
||||
trailingIcon="arrow-left"
|
||||
onClick={() => {
|
||||
setSelectedButtonId(null);
|
||||
setActiveTab('propertiesTab');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!isButtonDetailView && <span className="tj-text tj-header-h8 d-flex align-items-center">Edit Column</span>}
|
||||
{isButtonDetailView && <span className="tj-text tj-header-h8 d-flex align-items-center">Edit Button</span>}
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
iconOnly={true}
|
||||
isLucid={true}
|
||||
trailingIcon="copy"
|
||||
onClick={handleDuplicate}
|
||||
title={isButtonDetailView ? 'Duplicate button' : 'Duplicate column'}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
iconOnly={true}
|
||||
isLucid={true}
|
||||
trailingIcon="trash"
|
||||
onClick={handleDelete}
|
||||
title={isButtonDetailView ? 'Delete button' : 'Delete column'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex custom-gap-4 align-self-stretch tj-text tj-text-xsm font-weight-500 text-secondary cursor-pointer">
|
||||
<div
|
||||
className={`${activeTab === 'propertiesTab' && 'active-column-tab'} column-header-tab`}
|
||||
|
|
@ -125,6 +191,9 @@ export const ColumnPopoverContent = ({
|
|||
columnEventChanged={columnEventChanged}
|
||||
timeZoneOptions={timeZoneOptions}
|
||||
handleEventManagerPopoverCallback={handleEventManagerPopoverCallback}
|
||||
selectedButtonId={selectedButtonId}
|
||||
setSelectedButtonId={setSelectedButtonId}
|
||||
buttonManager={buttonManager}
|
||||
/>
|
||||
) : (
|
||||
<StylesTabElements
|
||||
|
|
@ -135,6 +204,8 @@ export const ColumnPopoverContent = ({
|
|||
onColumnItemChange={onColumnItemChange}
|
||||
getPopoverFieldSource={getPopoverFieldSource}
|
||||
component={component}
|
||||
selectedButtonId={selectedButtonId}
|
||||
buttonManager={buttonManager}
|
||||
/>
|
||||
)}
|
||||
</Popover.Body>
|
||||
|
|
|
|||
|
|
@ -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 && <DeprecatedColumnTypeMsg columnType={column.columnType} darkMode={darkMode} />}
|
||||
<div className="field px-3" data-cy={`dropdown-column-type`} onClick={(e) => e.stopPropagation()}>
|
||||
<label data-cy={`label-column-type`} className="form-label">
|
||||
{t('widget.Table.columnType', 'Column type')}
|
||||
</label>
|
||||
{!selectedButtonId && (
|
||||
<>
|
||||
{column.columnType && <DeprecatedColumnTypeMsg columnType={column.columnType} darkMode={darkMode} />}
|
||||
<div className="field px-3" data-cy={`dropdown-column-type`} onClick={(e) => e.stopPropagation()}>
|
||||
<label data-cy={`label-column-type`} className="form-label">
|
||||
{t('widget.Table.columnType', 'Column type')}
|
||||
</label>
|
||||
|
||||
<CustomSelect
|
||||
options={[
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'Date Picker', value: 'datepicker' },
|
||||
{ label: 'Select', value: 'select' },
|
||||
{ label: 'MultiSelect', value: 'newMultiSelect' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Image', value: 'image' },
|
||||
{ label: 'Link', value: 'link' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
{ label: 'Markdown', value: 'markdown' },
|
||||
{ label: 'HTML', value: 'html' },
|
||||
{ label: 'Rating', value: 'rating' },
|
||||
// Following column types are deprecated
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Dropdown', value: 'dropdown' },
|
||||
{ label: 'Multiselect', value: 'multiselect' },
|
||||
{ label: 'Toggle switch', value: 'toggle' },
|
||||
{ label: 'Radio', value: 'radio' },
|
||||
{ label: 'Badge', value: 'badge' },
|
||||
{ label: 'Multiple badges', value: 'badges' },
|
||||
{ label: 'Tags', value: 'tags' },
|
||||
]}
|
||||
components={{
|
||||
DropdownIndicator,
|
||||
Option: CustomOption,
|
||||
SingleValue: CustomValueContainer,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onColumnItemChange(index, 'columnType', value);
|
||||
}}
|
||||
value={column.columnType}
|
||||
useCustomStyles={true}
|
||||
styles={customStylesForSelect}
|
||||
className={`column-type-table-inspector`}
|
||||
/>
|
||||
</div>
|
||||
<div className="field px-3" data-cy={`input-and-label-column-name`}>
|
||||
<label data-cy={`label-column-name`} className="form-label">
|
||||
{t('widget.Table.columnName', 'Column name')}
|
||||
</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={column.name}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={column.name}
|
||||
onChange={(value) => onColumnItemChange(index, 'name', value)}
|
||||
componentName={getPopoverFieldSource(column.columnType, 'name')}
|
||||
popOverCallback={(showing) => {
|
||||
setColumnPopoverRootCloseBlocker('name', showing);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div data-cy={`input-and-label-key`} className="field px-3">
|
||||
<label className="form-label">{t('widget.Table.key', 'Key')}</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={column.key}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={column.name}
|
||||
onChange={(value) => onColumnItemChange(index, 'key', value)}
|
||||
componentName={getPopoverFieldSource(column.columnType, 'key')}
|
||||
popOverCallback={(showing) => {
|
||||
setColumnPopoverRootCloseBlocker('tableKey', showing);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div data-cy={`transformation-field`} className="field px-3">
|
||||
<label className="form-label">{t('widget.Table.transformationField', 'Transformation')}</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={column?.transformation ?? '{{cellValue}}'}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={column.name}
|
||||
onChange={(value) => onColumnItemChange(index, 'transformation', value)}
|
||||
componentName={getPopoverFieldSource(column.columnType, 'transformation')}
|
||||
popOverCallback={(showing) => {
|
||||
setColumnPopoverRootCloseBlocker('transformation', showing);
|
||||
}}
|
||||
enablePreview={false}
|
||||
/>
|
||||
</div>
|
||||
<CustomSelect
|
||||
options={[
|
||||
{ label: 'String', value: 'string' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'Date Picker', value: 'datepicker' },
|
||||
{ label: 'Select', value: 'select' },
|
||||
{ label: 'MultiSelect', value: 'newMultiSelect' },
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Image', value: 'image' },
|
||||
{ label: 'Link', value: 'link' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
{ label: 'Markdown', value: 'markdown' },
|
||||
{ label: 'HTML', value: 'html' },
|
||||
{ label: 'Rating', value: 'rating' },
|
||||
{ label: 'Button', value: 'button' },
|
||||
// Following column types are deprecated
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Dropdown', value: 'dropdown' },
|
||||
{ label: 'Multiselect', value: 'multiselect' },
|
||||
{ label: 'Toggle switch', value: 'toggle' },
|
||||
{ label: 'Radio', value: 'radio' },
|
||||
{ label: 'Badge', value: 'badge' },
|
||||
{ label: 'Multiple badges', value: 'badges' },
|
||||
{ label: 'Tags', value: 'tags' },
|
||||
]}
|
||||
components={{
|
||||
DropdownIndicator,
|
||||
Option: CustomOption,
|
||||
SingleValue: CustomValueContainer,
|
||||
}}
|
||||
onChange={(value) => {
|
||||
onColumnItemChange(index, 'columnType', value);
|
||||
}}
|
||||
value={column.columnType}
|
||||
useCustomStyles={true}
|
||||
styles={customStylesForSelect}
|
||||
className={`column-type-table-inspector`}
|
||||
/>
|
||||
</div>
|
||||
<div className="field px-3" data-cy={`input-and-label-column-name`}>
|
||||
<label data-cy={`label-column-name`} className="form-label">
|
||||
{t('widget.Table.columnName', 'Column name')}
|
||||
</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={column.name}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={column.name}
|
||||
onChange={(value) => onColumnItemChange(index, 'name', value)}
|
||||
componentName={getPopoverFieldSource(column.columnType, 'name')}
|
||||
popOverCallback={(showing) => {
|
||||
setColumnPopoverRootCloseBlocker('name', showing);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div data-cy={`input-and-label-key`} className="field px-3">
|
||||
<label className="form-label">{t('widget.Table.key', 'Key')}</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={column.key}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={column.name}
|
||||
onChange={(value) => onColumnItemChange(index, 'key', value)}
|
||||
componentName={getPopoverFieldSource(column.columnType, 'key')}
|
||||
popOverCallback={(showing) => {
|
||||
setColumnPopoverRootCloseBlocker('tableKey', showing);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{column.columnType !== 'button' && (
|
||||
<div data-cy={`transformation-field`} className="field px-3">
|
||||
<label className="form-label">{t('widget.Table.transformationField', 'Transformation')}</label>
|
||||
<CodeHinter
|
||||
currentState={currentState}
|
||||
initialValue={column?.transformation ?? '{{cellValue}}'}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={column.name}
|
||||
onChange={(value) => onColumnItemChange(index, 'transformation', value)}
|
||||
componentName={getPopoverFieldSource(column.columnType, 'transformation')}
|
||||
popOverCallback={(showing) => {
|
||||
setColumnPopoverRootCloseBlocker('transformation', showing);
|
||||
}}
|
||||
enablePreview={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{column.columnType === 'rating' && (
|
||||
<div data-cy={`rating-type-field`} className="field px-3 d-flex justify-content-between">
|
||||
<label className="form-label d-flex align-items-center">{'Icon'}</label>
|
||||
|
|
@ -193,6 +206,7 @@ export const PropertiesTabElements = ({
|
|||
<EventManager
|
||||
sourceId={props?.component?.id}
|
||||
eventSourceType="table_column"
|
||||
customEventRefs={{ ref: column.name }}
|
||||
hideEmptyEventsAlert={true}
|
||||
eventMetaDefinition={{ events: { onChange: { displayName: 'On change' } } }}
|
||||
currentState={currentState}
|
||||
|
|
@ -207,6 +221,47 @@ export const PropertiesTabElements = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{column.columnType === 'button' && !selectedButtonId && (
|
||||
<>
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Visibility"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={onColumnItemChange}
|
||||
property="columnVisibility"
|
||||
props={column}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Visibility' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonListManager
|
||||
buttons={column.buttons || []}
|
||||
onAddButton={addButton}
|
||||
onReorderButtons={reorderButtons}
|
||||
onSelectButton={setSelectedButtonId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{column.columnType === 'button' && selectedButtonId && (
|
||||
<ButtonPropertiesTab
|
||||
button={getButton(selectedButtonId)}
|
||||
column={column}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
currentState={currentState}
|
||||
onButtonPropertyChange={(property, value) => 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 = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{!['image', 'link'].includes(column.columnType) && (
|
||||
{!['image', 'link', 'button'].includes(column.columnType) && (
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
|
|
@ -350,22 +405,24 @@ export const PropertiesTabElements = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Visibility"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={onColumnItemChange}
|
||||
property="columnVisibility"
|
||||
props={column}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Visibility' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
{column.columnType !== 'button' && (
|
||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Visibility"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={onColumnItemChange}
|
||||
property="columnVisibility"
|
||||
props={column}
|
||||
component={component}
|
||||
paramMeta={{ type: 'toggle', displayName: 'Visibility' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{['select', 'newMultiSelect', 'datepicker', 'rating'].includes(column.columnType) && <hr className="mx-0 my-2" />}
|
||||
{column.columnType === 'datepicker' && (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<div className="field d-flex custom-gap-12 align-items-center align-self-stretch justify-content-between px-3">
|
||||
<label className="d-flex align-items-center" style={{ flex: '1 1 0' }}>
|
||||
{column.columnType !== 'boolean' && column.columnType !== 'image' && column.columnType !== 'rating'
|
||||
? t('widget.Table.textAlignment', 'Text Alignment')
|
||||
: 'Alignment'}
|
||||
</label>
|
||||
<ToggleGroup
|
||||
onValueChange={(_value) => onColumnItemChange(index, 'horizontalAlignment', _value)}
|
||||
defaultValue={column?.horizontalAlignment || 'left'}
|
||||
style={{ width: '58%' }}
|
||||
>
|
||||
<ToggleGroupItem value="left">
|
||||
<AlignLeft width={14} />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="center">
|
||||
<AlignCenter width={14} />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="right">
|
||||
<AlignRight width={14} />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
{column.columnType !== 'button' && (
|
||||
<div className="field d-flex custom-gap-12 align-items-center align-self-stretch justify-content-between px-3">
|
||||
<label className="d-flex align-items-center" style={{ flex: '1 1 0' }}>
|
||||
{column.columnType !== 'boolean' && column.columnType !== 'image' && column.columnType !== 'rating'
|
||||
? t('widget.Table.textAlignment', 'Text Alignment')
|
||||
: 'Alignment'}
|
||||
</label>
|
||||
<ToggleGroup
|
||||
onValueChange={(_value) => onColumnItemChange(index, 'horizontalAlignment', _value)}
|
||||
defaultValue={column?.horizontalAlignment || 'left'}
|
||||
style={{ width: '58%' }}
|
||||
>
|
||||
<ToggleGroupItem value="left">
|
||||
<AlignLeft width={14} />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="center">
|
||||
<AlignCenter width={14} />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="right">
|
||||
<AlignRight width={14} />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)}
|
||||
{column.columnType === 'toggle' && (
|
||||
<div>
|
||||
<div className="field px-3">
|
||||
|
|
@ -253,6 +259,37 @@ export const StylesTabElements = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{column.columnType === 'button' && !selectedButtonId && (
|
||||
<div className="d-flex flex-column custom-gap-16">
|
||||
<div className="field px-3" data-cy={`input-and-label-cell-background-color`}>
|
||||
<ProgramaticallyHandleProperties
|
||||
label="Cell color"
|
||||
currentState={currentState}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
callbackFunction={onColumnItemChange}
|
||||
property="cellBackgroundColor"
|
||||
props={column}
|
||||
component={component}
|
||||
paramMeta={{ type: 'colorSwatches', displayName: 'Cell color' }}
|
||||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{column.columnType === 'button' && selectedButtonId && (
|
||||
<ButtonStylesTab
|
||||
button={getButton(selectedButtonId)}
|
||||
index={index}
|
||||
darkMode={darkMode}
|
||||
currentState={currentState}
|
||||
onButtonPropertyChange={(property, value) => updateButtonProperty(selectedButtonId, property, value)}
|
||||
onButtonPropertiesChange={(updates) => updateButtonProperties(selectedButtonId, updates)}
|
||||
component={component}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`)}
|
||||
/>
|
||||
</Popover>
|
||||
),
|
||||
|
|
@ -166,6 +171,8 @@ export const Table = (props) => {
|
|||
props,
|
||||
handleColumnEventChange,
|
||||
handleEventManagerPopoverCallback,
|
||||
duplicateColumn,
|
||||
removeColumn,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -488,6 +495,7 @@ export const Table = (props) => {
|
|||
<OverlayTrigger
|
||||
trigger="click"
|
||||
placement="left"
|
||||
flip={true}
|
||||
rootClose={isRootCloseEnabled}
|
||||
overlay={renderColumnPopover(item, index)}
|
||||
onToggle={(show) => handleToggleColumnPopover(index, show)}
|
||||
|
|
@ -554,9 +562,27 @@ export const Table = (props) => {
|
|||
</div>
|
||||
),
|
||||
},
|
||||
// Action buttons section
|
||||
// Action buttons section (deprecated — replaced by button column type)
|
||||
{
|
||||
title: 'Action buttons',
|
||||
title: (
|
||||
<div className="d-flex flex-row align-items-center" style={{ gap: '6px' }}>
|
||||
<span>Action buttons</span>
|
||||
<ToolTip
|
||||
message={
|
||||
<div style={{ padding: '8px 4px', textAlign: 'left', width: '185px' }}>
|
||||
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.
|
||||
</div>
|
||||
}
|
||||
show={true}
|
||||
placement="bottom"
|
||||
>
|
||||
<span>
|
||||
<Icon name={'warning'} height={14} width={14} fill="#DB4324" />
|
||||
</span>
|
||||
</ToolTip>
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="field">
|
||||
<div className="row g-2">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
const ButtonTypeIcon = ({ fill = '#ACB2B9', width = '16', className = '', viewBox = '0 0 16 16', style, height }) => (
|
||||
<svg
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={style}
|
||||
>
|
||||
<rect x="1.5" y="4.5" width="13" height="7" rx="2" stroke={fill} strokeWidth="1.2" />
|
||||
<path d="M5.5 8H10.5" stroke={fill} strokeWidth="1.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ButtonTypeIcon;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = <TablerIcon iconName={iconName} size={14} stroke={1.5} style={{ color: computedIconColor }} />;
|
||||
}
|
||||
|
||||
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 = (
|
||||
<Button
|
||||
variant={variant}
|
||||
size="default"
|
||||
isLoading={!!loadingState}
|
||||
disabled={!!disableButton}
|
||||
fill={computedLoaderColor || undefined}
|
||||
style={buttonStyle}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{iconAlignment === 'left' && iconElement}
|
||||
{buttonLabel || 'Button'}
|
||||
{iconAlignment === 'right' && iconElement}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const hasTooltip = tooltip && tooltip.toString().trim();
|
||||
|
||||
if (hasTooltip) {
|
||||
return (
|
||||
<OverlayTrigger placement="auto" delay={{ show: 500, hide: 0 }} overlay={<Tooltip>{tooltip}</Tooltip>}>
|
||||
<div style={{ display: 'flex' }}>{buttonElement}</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
return buttonElement;
|
||||
};
|
||||
|
||||
export default ButtonColumn;
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
className="h-100 d-flex align-items-center"
|
||||
style={{
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
{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 (
|
||||
<ButtonColumn
|
||||
key={button.id}
|
||||
buttonLabel={resolvedLabel}
|
||||
buttonType={resolvedType}
|
||||
disableButton={resolvedDisable}
|
||||
loadingState={resolvedLoading}
|
||||
backgroundColor={resolvedBgColor}
|
||||
labelColor={resolvedLabelColor}
|
||||
iconName={resolvedIconName}
|
||||
iconVisibility={resolvedIconVisibility}
|
||||
iconColor={resolvedIconColor}
|
||||
iconAlignment={resolvedIconAlignment}
|
||||
loaderColor={resolvedLoaderColor}
|
||||
borderColor={resolvedBorderColor}
|
||||
borderRadius={resolvedBorderRadius}
|
||||
tooltip={resolvedTooltip}
|
||||
onClick={() => {
|
||||
if (onClick) onClick(button.id, getTableColumnEvents(id));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonColumnGroup;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<td
|
||||
key={cell.id}
|
||||
data-cy={`${generateCypressDataCy(componentName)}-${generateCypressDataCy(typeof cell.column.columnDef?.header === 'string' ? cell.column.columnDef?.header : cell.column.id)}-row-${virtualRow.index}`}
|
||||
data-cy={`${generateCypressDataCy(componentName)}-${generateCypressDataCy(
|
||||
typeof cell.column.columnDef?.header === 'string' ? cell.column.columnDef?.header : cell.column.id
|
||||
)}-row-${virtualRow.index}`}
|
||||
style={cellStyles}
|
||||
className={cx('table-cell td', {
|
||||
'has-actions': cell.column.id === 'rightActions' || cell.column.id === 'leftActions',
|
||||
'has-actions':
|
||||
cell.column.id === 'rightActions' ||
|
||||
cell.column.id === 'leftActions' ||
|
||||
cell.column.columnDef?.meta?.columnType === 'button',
|
||||
'has-left-actions': cell.column.id === 'leftActions',
|
||||
'has-right-actions': cell.column.id === 'rightActions',
|
||||
'table-text-align-center': cell.column.columnDef?.meta?.horizontalAlignment === 'center',
|
||||
|
|
@ -130,8 +138,9 @@ export const TableRow = ({
|
|||
}}
|
||||
>
|
||||
<div
|
||||
className={`td-container ${cell.column.columnDef?.meta?.columnType === 'image' && 'jet-table-image-column h-100'
|
||||
} ${cell.column.columnDef?.meta?.columnType !== 'image' && `w-100 h-100`}`}
|
||||
className={`td-container ${
|
||||
cell.column.columnDef?.meta?.columnType === 'image' && 'jet-table-image-column h-100'
|
||||
} ${cell.column.columnDef?.meta?.columnType !== 'image' && `w-100 h-100`}`}
|
||||
>
|
||||
{flexRender(cell.column?.columnDef?.cell, cell.getContext())}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 <span />;
|
||||
const columnKey = column?.key || column?.name;
|
||||
const buttons = column.buttons || [];
|
||||
|
||||
return (
|
||||
<ButtonColumnGroup
|
||||
id={id}
|
||||
buttons={buttons}
|
||||
cellBackgroundColor={getResolvedValue(column.cellBackgroundColor, { cellValue, rowData })}
|
||||
cellValue={cellValue}
|
||||
rowData={rowData}
|
||||
onClick={(buttonId, tableColumnEvents) => {
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue