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 { StylesTabElements } from './StylesTabElements';
|
||||||
import { PropertiesTabElements } from './PropertiesTabElements';
|
import { PropertiesTabElements } from './PropertiesTabElements';
|
||||||
import { TableColumnContext } from './TableColumnContext';
|
import { TableColumnContext } from './TableColumnContext';
|
||||||
|
import { Button } from '@/components/ui/Button/Button';
|
||||||
|
import { useButtonManager } from '../hooks/useButtonManager';
|
||||||
|
|
||||||
export const ColumnPopoverContent = ({
|
export const ColumnPopoverContent = ({
|
||||||
column,
|
column,
|
||||||
|
|
@ -16,8 +18,11 @@ export const ColumnPopoverContent = ({
|
||||||
props,
|
props,
|
||||||
columnEventChanged,
|
columnEventChanged,
|
||||||
handleEventManagerPopoverCallback,
|
handleEventManagerPopoverCallback,
|
||||||
|
onDuplicateColumn,
|
||||||
|
onDeleteColumn,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState('propertiesTab');
|
const [activeTab, setActiveTab] = useState('propertiesTab');
|
||||||
|
const [selectedButtonId, setSelectedButtonId] = useState(null);
|
||||||
const [isGoingBelowScreen, setIsGoingBelowScreen] = useState(false);
|
const [isGoingBelowScreen, setIsGoingBelowScreen] = useState(false);
|
||||||
const popoverRef = useRef(null);
|
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);
|
const timeoutId = setTimeout(checkPopoverPosition, 100);
|
||||||
|
|
||||||
// Also check on window resize
|
|
||||||
window.addEventListener('resize', checkPopoverPosition);
|
window.addEventListener('resize', checkPopoverPosition);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -80,6 +82,29 @@ export const ColumnPopoverContent = ({
|
||||||
};
|
};
|
||||||
}, [index]);
|
}, [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,
|
/* TableColumnContext provides the table's component ID to all nested CodeHinters,
|
||||||
enabling rowData/cellValue autocomplete hints in column editors (properties, styles, etc.).
|
enabling rowData/cellValue autocomplete hints in column editors (properties, styles, etc.).
|
||||||
|
|
@ -87,7 +112,48 @@ export const ColumnPopoverContent = ({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TableColumnContext.Provider value={component?.id}>
|
<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="d-flex custom-gap-4 align-self-stretch tj-text tj-text-xsm font-weight-500 text-secondary cursor-pointer">
|
||||||
<div
|
<div
|
||||||
className={`${activeTab === 'propertiesTab' && 'active-column-tab'} column-header-tab`}
|
className={`${activeTab === 'propertiesTab' && 'active-column-tab'} column-header-tab`}
|
||||||
|
|
@ -125,6 +191,9 @@ export const ColumnPopoverContent = ({
|
||||||
columnEventChanged={columnEventChanged}
|
columnEventChanged={columnEventChanged}
|
||||||
timeZoneOptions={timeZoneOptions}
|
timeZoneOptions={timeZoneOptions}
|
||||||
handleEventManagerPopoverCallback={handleEventManagerPopoverCallback}
|
handleEventManagerPopoverCallback={handleEventManagerPopoverCallback}
|
||||||
|
selectedButtonId={selectedButtonId}
|
||||||
|
setSelectedButtonId={setSelectedButtonId}
|
||||||
|
buttonManager={buttonManager}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StylesTabElements
|
<StylesTabElements
|
||||||
|
|
@ -135,6 +204,8 @@ export const ColumnPopoverContent = ({
|
||||||
onColumnItemChange={onColumnItemChange}
|
onColumnItemChange={onColumnItemChange}
|
||||||
getPopoverFieldSource={getPopoverFieldSource}
|
getPopoverFieldSource={getPopoverFieldSource}
|
||||||
component={component}
|
component={component}
|
||||||
|
selectedButtonId={selectedButtonId}
|
||||||
|
buttonManager={buttonManager}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popover.Body>
|
</Popover.Body>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import Check from '@/_ui/Icon/solidIcons/Check';
|
||||||
import Icon from '@/_ui/Icon/solidIcons/index';
|
import Icon from '@/_ui/Icon/solidIcons/index';
|
||||||
import RatingIconToggle from './RatingColumn/RatingIconToggle';
|
import RatingIconToggle from './RatingColumn/RatingIconToggle';
|
||||||
import RatingColumnProperties from './RatingColumn/RatingColumnProperties';
|
import RatingColumnProperties from './RatingColumn/RatingColumnProperties';
|
||||||
|
import { ButtonListManager } from './ButtonListManager';
|
||||||
|
import { ButtonPropertiesTab } from './ButtonPropertiesTab';
|
||||||
|
|
||||||
const CustomOption = (props) => {
|
const CustomOption = (props) => {
|
||||||
const ColumnIcon = getColumnIcon(props.data.value);
|
const ColumnIcon = getColumnIcon(props.data.value);
|
||||||
|
|
@ -74,8 +76,12 @@ export const PropertiesTabElements = ({
|
||||||
columnEventChanged,
|
columnEventChanged,
|
||||||
timeZoneOptions,
|
timeZoneOptions,
|
||||||
handleEventManagerPopoverCallback,
|
handleEventManagerPopoverCallback,
|
||||||
|
selectedButtonId,
|
||||||
|
setSelectedButtonId,
|
||||||
|
buttonManager,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { addButton, removeButton, updateButtonProperty, reorderButtons, getButton } = buttonManager;
|
||||||
|
|
||||||
const customStylesForSelect = {
|
const customStylesForSelect = {
|
||||||
...defaultStyles(darkMode, '100%'),
|
...defaultStyles(darkMode, '100%'),
|
||||||
|
|
@ -83,102 +89,109 @@ export const PropertiesTabElements = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{column.columnType && <DeprecatedColumnTypeMsg columnType={column.columnType} darkMode={darkMode} />}
|
{!selectedButtonId && (
|
||||||
<div className="field px-3" data-cy={`dropdown-column-type`} onClick={(e) => e.stopPropagation()}>
|
<>
|
||||||
<label data-cy={`label-column-type`} className="form-label">
|
{column.columnType && <DeprecatedColumnTypeMsg columnType={column.columnType} darkMode={darkMode} />}
|
||||||
{t('widget.Table.columnType', 'Column type')}
|
<div className="field px-3" data-cy={`dropdown-column-type`} onClick={(e) => e.stopPropagation()}>
|
||||||
</label>
|
<label data-cy={`label-column-type`} className="form-label">
|
||||||
|
{t('widget.Table.columnType', 'Column type')}
|
||||||
|
</label>
|
||||||
|
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
options={[
|
options={[
|
||||||
{ label: 'String', value: 'string' },
|
{ label: 'String', value: 'string' },
|
||||||
{ label: 'Number', value: 'number' },
|
{ label: 'Number', value: 'number' },
|
||||||
{ label: 'Text', value: 'text' },
|
{ label: 'Text', value: 'text' },
|
||||||
{ label: 'Date Picker', value: 'datepicker' },
|
{ label: 'Date Picker', value: 'datepicker' },
|
||||||
{ label: 'Select', value: 'select' },
|
{ label: 'Select', value: 'select' },
|
||||||
{ label: 'MultiSelect', value: 'newMultiSelect' },
|
{ label: 'MultiSelect', value: 'newMultiSelect' },
|
||||||
{ label: 'Boolean', value: 'boolean' },
|
{ label: 'Boolean', value: 'boolean' },
|
||||||
{ label: 'Image', value: 'image' },
|
{ label: 'Image', value: 'image' },
|
||||||
{ label: 'Link', value: 'link' },
|
{ label: 'Link', value: 'link' },
|
||||||
{ label: 'JSON', value: 'json' },
|
{ label: 'JSON', value: 'json' },
|
||||||
{ label: 'Markdown', value: 'markdown' },
|
{ label: 'Markdown', value: 'markdown' },
|
||||||
{ label: 'HTML', value: 'html' },
|
{ label: 'HTML', value: 'html' },
|
||||||
{ label: 'Rating', value: 'rating' },
|
{ label: 'Rating', value: 'rating' },
|
||||||
// Following column types are deprecated
|
{ label: 'Button', value: 'button' },
|
||||||
{ label: 'Default', value: 'default' },
|
// Following column types are deprecated
|
||||||
{ label: 'Dropdown', value: 'dropdown' },
|
{ label: 'Default', value: 'default' },
|
||||||
{ label: 'Multiselect', value: 'multiselect' },
|
{ label: 'Dropdown', value: 'dropdown' },
|
||||||
{ label: 'Toggle switch', value: 'toggle' },
|
{ label: 'Multiselect', value: 'multiselect' },
|
||||||
{ label: 'Radio', value: 'radio' },
|
{ label: 'Toggle switch', value: 'toggle' },
|
||||||
{ label: 'Badge', value: 'badge' },
|
{ label: 'Radio', value: 'radio' },
|
||||||
{ label: 'Multiple badges', value: 'badges' },
|
{ label: 'Badge', value: 'badge' },
|
||||||
{ label: 'Tags', value: 'tags' },
|
{ label: 'Multiple badges', value: 'badges' },
|
||||||
]}
|
{ label: 'Tags', value: 'tags' },
|
||||||
components={{
|
]}
|
||||||
DropdownIndicator,
|
components={{
|
||||||
Option: CustomOption,
|
DropdownIndicator,
|
||||||
SingleValue: CustomValueContainer,
|
Option: CustomOption,
|
||||||
}}
|
SingleValue: CustomValueContainer,
|
||||||
onChange={(value) => {
|
}}
|
||||||
onColumnItemChange(index, 'columnType', value);
|
onChange={(value) => {
|
||||||
}}
|
onColumnItemChange(index, 'columnType', value);
|
||||||
value={column.columnType}
|
}}
|
||||||
useCustomStyles={true}
|
value={column.columnType}
|
||||||
styles={customStylesForSelect}
|
useCustomStyles={true}
|
||||||
className={`column-type-table-inspector`}
|
styles={customStylesForSelect}
|
||||||
/>
|
className={`column-type-table-inspector`}
|
||||||
</div>
|
/>
|
||||||
<div className="field px-3" data-cy={`input-and-label-column-name`}>
|
</div>
|
||||||
<label data-cy={`label-column-name`} className="form-label">
|
<div className="field px-3" data-cy={`input-and-label-column-name`}>
|
||||||
{t('widget.Table.columnName', 'Column name')}
|
<label data-cy={`label-column-name`} className="form-label">
|
||||||
</label>
|
{t('widget.Table.columnName', 'Column name')}
|
||||||
<CodeHinter
|
</label>
|
||||||
currentState={currentState}
|
<CodeHinter
|
||||||
initialValue={column.name}
|
currentState={currentState}
|
||||||
theme={darkMode ? 'monokai' : 'default'}
|
initialValue={column.name}
|
||||||
mode="javascript"
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
lineNumbers={false}
|
mode="javascript"
|
||||||
placeholder={column.name}
|
lineNumbers={false}
|
||||||
onChange={(value) => onColumnItemChange(index, 'name', value)}
|
placeholder={column.name}
|
||||||
componentName={getPopoverFieldSource(column.columnType, 'name')}
|
onChange={(value) => onColumnItemChange(index, 'name', value)}
|
||||||
popOverCallback={(showing) => {
|
componentName={getPopoverFieldSource(column.columnType, 'name')}
|
||||||
setColumnPopoverRootCloseBlocker('name', showing);
|
popOverCallback={(showing) => {
|
||||||
}}
|
setColumnPopoverRootCloseBlocker('name', showing);
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
<div data-cy={`input-and-label-key`} className="field px-3">
|
</div>
|
||||||
<label className="form-label">{t('widget.Table.key', 'Key')}</label>
|
<div data-cy={`input-and-label-key`} className="field px-3">
|
||||||
<CodeHinter
|
<label className="form-label">{t('widget.Table.key', 'Key')}</label>
|
||||||
currentState={currentState}
|
<CodeHinter
|
||||||
initialValue={column.key}
|
currentState={currentState}
|
||||||
theme={darkMode ? 'monokai' : 'default'}
|
initialValue={column.key}
|
||||||
mode="javascript"
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
lineNumbers={false}
|
mode="javascript"
|
||||||
placeholder={column.name}
|
lineNumbers={false}
|
||||||
onChange={(value) => onColumnItemChange(index, 'key', value)}
|
placeholder={column.name}
|
||||||
componentName={getPopoverFieldSource(column.columnType, 'key')}
|
onChange={(value) => onColumnItemChange(index, 'key', value)}
|
||||||
popOverCallback={(showing) => {
|
componentName={getPopoverFieldSource(column.columnType, 'key')}
|
||||||
setColumnPopoverRootCloseBlocker('tableKey', showing);
|
popOverCallback={(showing) => {
|
||||||
}}
|
setColumnPopoverRootCloseBlocker('tableKey', showing);
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
<div data-cy={`transformation-field`} className="field px-3">
|
</div>
|
||||||
<label className="form-label">{t('widget.Table.transformationField', 'Transformation')}</label>
|
</>
|
||||||
<CodeHinter
|
)}
|
||||||
currentState={currentState}
|
{column.columnType !== 'button' && (
|
||||||
initialValue={column?.transformation ?? '{{cellValue}}'}
|
<div data-cy={`transformation-field`} className="field px-3">
|
||||||
theme={darkMode ? 'monokai' : 'default'}
|
<label className="form-label">{t('widget.Table.transformationField', 'Transformation')}</label>
|
||||||
mode="javascript"
|
<CodeHinter
|
||||||
lineNumbers={false}
|
currentState={currentState}
|
||||||
placeholder={column.name}
|
initialValue={column?.transformation ?? '{{cellValue}}'}
|
||||||
onChange={(value) => onColumnItemChange(index, 'transformation', value)}
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
componentName={getPopoverFieldSource(column.columnType, 'transformation')}
|
mode="javascript"
|
||||||
popOverCallback={(showing) => {
|
lineNumbers={false}
|
||||||
setColumnPopoverRootCloseBlocker('transformation', showing);
|
placeholder={column.name}
|
||||||
}}
|
onChange={(value) => onColumnItemChange(index, 'transformation', value)}
|
||||||
enablePreview={false}
|
componentName={getPopoverFieldSource(column.columnType, 'transformation')}
|
||||||
/>
|
popOverCallback={(showing) => {
|
||||||
</div>
|
setColumnPopoverRootCloseBlocker('transformation', showing);
|
||||||
|
}}
|
||||||
|
enablePreview={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{column.columnType === 'rating' && (
|
{column.columnType === 'rating' && (
|
||||||
<div data-cy={`rating-type-field`} className="field px-3 d-flex justify-content-between">
|
<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>
|
<label className="form-label d-flex align-items-center">{'Icon'}</label>
|
||||||
|
|
@ -193,6 +206,7 @@ export const PropertiesTabElements = ({
|
||||||
<EventManager
|
<EventManager
|
||||||
sourceId={props?.component?.id}
|
sourceId={props?.component?.id}
|
||||||
eventSourceType="table_column"
|
eventSourceType="table_column"
|
||||||
|
customEventRefs={{ ref: column.name }}
|
||||||
hideEmptyEventsAlert={true}
|
hideEmptyEventsAlert={true}
|
||||||
eventMetaDefinition={{ events: { onChange: { displayName: 'On change' } } }}
|
eventMetaDefinition={{ events: { onChange: { displayName: 'On change' } } }}
|
||||||
currentState={currentState}
|
currentState={currentState}
|
||||||
|
|
@ -207,6 +221,47 @@ export const PropertiesTabElements = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 === 'dropdown' ||
|
||||||
column.columnType === 'multiselect' ||
|
column.columnType === 'multiselect' ||
|
||||||
column.columnType === 'badge' ||
|
column.columnType === 'badge' ||
|
||||||
|
|
@ -302,7 +357,7 @@ export const PropertiesTabElements = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px' }}>
|
||||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||||
<ProgramaticallyHandleProperties
|
<ProgramaticallyHandleProperties
|
||||||
|
|
@ -350,22 +405,24 @@ export const PropertiesTabElements = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
{column.columnType !== 'button' && (
|
||||||
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
<div className="border mx-3 column-popover-card-ui" style={{ borderRadius: '6px', marginTop: '-8px' }}>
|
||||||
<ProgramaticallyHandleProperties
|
<div style={{ background: 'var(--surfaces-surface-02)', padding: '8px 12px' }}>
|
||||||
label="Visibility"
|
<ProgramaticallyHandleProperties
|
||||||
currentState={currentState}
|
label="Visibility"
|
||||||
index={index}
|
currentState={currentState}
|
||||||
darkMode={darkMode}
|
index={index}
|
||||||
callbackFunction={onColumnItemChange}
|
darkMode={darkMode}
|
||||||
property="columnVisibility"
|
callbackFunction={onColumnItemChange}
|
||||||
props={column}
|
property="columnVisibility"
|
||||||
component={component}
|
props={column}
|
||||||
paramMeta={{ type: 'toggle', displayName: 'Visibility' }}
|
component={component}
|
||||||
paramType="properties"
|
paramMeta={{ type: 'toggle', displayName: 'Visibility' }}
|
||||||
/>
|
paramType="properties"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{['select', 'newMultiSelect', 'datepicker', 'rating'].includes(column.columnType) && <hr className="mx-0 my-2" />}
|
{['select', 'newMultiSelect', 'datepicker', 'rating'].includes(column.columnType) && <hr className="mx-0 my-2" />}
|
||||||
{column.columnType === 'datepicker' && (
|
{column.columnType === 'datepicker' && (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import AlignCenter from '@/_ui/Icon/solidIcons/AlignCenter';
|
||||||
import AlignRight from '@/_ui/Icon/solidIcons/AlignRight';
|
import AlignRight from '@/_ui/Icon/solidIcons/AlignRight';
|
||||||
import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties';
|
import { ProgramaticallyHandleProperties } from '../ProgramaticallyHandleProperties';
|
||||||
import { Select } from '@/AppBuilder/CodeBuilder/Elements/Select';
|
import { Select } from '@/AppBuilder/CodeBuilder/Elements/Select';
|
||||||
|
import { ButtonStylesTab } from './ButtonStylesTab';
|
||||||
|
|
||||||
export const StylesTabElements = ({
|
export const StylesTabElements = ({
|
||||||
column,
|
column,
|
||||||
|
|
@ -18,32 +19,37 @@ export const StylesTabElements = ({
|
||||||
onColumnItemChange,
|
onColumnItemChange,
|
||||||
getPopoverFieldSource,
|
getPopoverFieldSource,
|
||||||
component,
|
component,
|
||||||
|
selectedButtonId,
|
||||||
|
buttonManager,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { updateButtonProperty, updateButtonProperties, getButton } = buttonManager;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="field d-flex custom-gap-12 align-items-center align-self-stretch justify-content-between px-3">
|
{column.columnType !== 'button' && (
|
||||||
<label className="d-flex align-items-center" style={{ flex: '1 1 0' }}>
|
<div className="field d-flex custom-gap-12 align-items-center align-self-stretch justify-content-between px-3">
|
||||||
{column.columnType !== 'boolean' && column.columnType !== 'image' && column.columnType !== 'rating'
|
<label className="d-flex align-items-center" style={{ flex: '1 1 0' }}>
|
||||||
? t('widget.Table.textAlignment', 'Text Alignment')
|
{column.columnType !== 'boolean' && column.columnType !== 'image' && column.columnType !== 'rating'
|
||||||
: 'Alignment'}
|
? t('widget.Table.textAlignment', 'Text Alignment')
|
||||||
</label>
|
: 'Alignment'}
|
||||||
<ToggleGroup
|
</label>
|
||||||
onValueChange={(_value) => onColumnItemChange(index, 'horizontalAlignment', _value)}
|
<ToggleGroup
|
||||||
defaultValue={column?.horizontalAlignment || 'left'}
|
onValueChange={(_value) => onColumnItemChange(index, 'horizontalAlignment', _value)}
|
||||||
style={{ width: '58%' }}
|
defaultValue={column?.horizontalAlignment || 'left'}
|
||||||
>
|
style={{ width: '58%' }}
|
||||||
<ToggleGroupItem value="left">
|
>
|
||||||
<AlignLeft width={14} />
|
<ToggleGroupItem value="left">
|
||||||
</ToggleGroupItem>
|
<AlignLeft width={14} />
|
||||||
<ToggleGroupItem value="center">
|
</ToggleGroupItem>
|
||||||
<AlignCenter width={14} />
|
<ToggleGroupItem value="center">
|
||||||
</ToggleGroupItem>
|
<AlignCenter width={14} />
|
||||||
<ToggleGroupItem value="right">
|
</ToggleGroupItem>
|
||||||
<AlignRight width={14} />
|
<ToggleGroupItem value="right">
|
||||||
</ToggleGroupItem>
|
<AlignRight width={14} />
|
||||||
</ToggleGroup>
|
</ToggleGroupItem>
|
||||||
</div>
|
</ToggleGroup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{column.columnType === 'toggle' && (
|
{column.columnType === 'toggle' && (
|
||||||
<div>
|
<div>
|
||||||
<div className="field px-3">
|
<div className="field px-3">
|
||||||
|
|
@ -253,6 +259,37 @@ export const StylesTabElements = ({
|
||||||
</div>
|
</div>
|
||||||
</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,
|
paramMeta,
|
||||||
}) => {
|
}) => {
|
||||||
const getValueBasedOnProperty = (property, props) => {
|
const getValueBasedOnProperty = (property, props) => {
|
||||||
switch (property) {
|
if (property === 'makeDefaultOption') {
|
||||||
case 'isEditable':
|
return props?.[index]?.makeDefaultOption;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return props?.[property];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitialValue = (property, definitionObj) => {
|
const getInitialValue = (property, definitionObj) => {
|
||||||
|
|
@ -84,7 +32,7 @@ export const ProgramaticallyHandleProperties = ({
|
||||||
return value || '{{true}}';
|
return value || '{{true}}';
|
||||||
}
|
}
|
||||||
if (property === 'cellBackgroundColor') {
|
if (property === 'cellBackgroundColor') {
|
||||||
return definitionObj?.value ?? '';
|
return definitionObj?.value || 'var(--cc-surface1-surface)';
|
||||||
}
|
}
|
||||||
if (property === 'textColor') {
|
if (property === 'textColor') {
|
||||||
return definitionObj?.value ?? '#11181C';
|
return definitionObj?.value ?? '#11181C';
|
||||||
|
|
@ -109,6 +57,33 @@ export const ProgramaticallyHandleProperties = ({
|
||||||
if (property === 'jsonIndentation') {
|
if (property === 'jsonIndentation') {
|
||||||
return definitionObj?.value ?? `{{true}}`;
|
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}}`;
|
return definitionObj?.value ?? `{{false}}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import NoListItem from './NoListItem';
|
||||||
import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties';
|
import { ProgramaticallyHandleProperties } from './ProgramaticallyHandleProperties';
|
||||||
import { ColumnPopoverContent } from './ColumnManager/ColumnPopover';
|
import { ColumnPopoverContent } from './ColumnManager/ColumnPopover';
|
||||||
import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg';
|
import { checkIfTableColumnDeprecated } from './ColumnManager/DeprecatedColumnTypeMsg';
|
||||||
|
import { ToolTip } from '@/_components/ToolTip';
|
||||||
|
import Icon from '@/_ui/Icon/solidIcons/index';
|
||||||
import { ColorSwatches } from '@/modules/Appbuilder/components';
|
import { ColorSwatches } from '@/modules/Appbuilder/components';
|
||||||
import { getColumnIcon } from './utils';
|
import { getColumnIcon } from './utils';
|
||||||
import { getSafeRenderableValue } from '@/AppBuilder/Widgets/utils';
|
import { getSafeRenderableValue } from '@/AppBuilder/Widgets/utils';
|
||||||
|
|
@ -45,6 +47,7 @@ const getColumnTypeDisplayText = (columnType) => {
|
||||||
json: 'JSON',
|
json: 'JSON',
|
||||||
markdown: 'Markdown',
|
markdown: 'Markdown',
|
||||||
html: 'HTML',
|
html: 'HTML',
|
||||||
|
button: 'Button',
|
||||||
};
|
};
|
||||||
return displayMap[columnType] ?? capitalize(columnType ?? '');
|
return displayMap[columnType] ?? capitalize(columnType ?? '');
|
||||||
};
|
};
|
||||||
|
|
@ -153,6 +156,8 @@ export const Table = (props) => {
|
||||||
props={props}
|
props={props}
|
||||||
columnEventChanged={handleColumnEventChange}
|
columnEventChanged={handleColumnEventChange}
|
||||||
handleEventManagerPopoverCallback={handleEventManagerPopoverCallback}
|
handleEventManagerPopoverCallback={handleEventManagerPopoverCallback}
|
||||||
|
onDuplicateColumn={() => duplicateColumn(index)}
|
||||||
|
onDeleteColumn={() => removeColumn(index, `${column.name}-${index}`)}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
),
|
),
|
||||||
|
|
@ -166,6 +171,8 @@ export const Table = (props) => {
|
||||||
props,
|
props,
|
||||||
handleColumnEventChange,
|
handleColumnEventChange,
|
||||||
handleEventManagerPopoverCallback,
|
handleEventManagerPopoverCallback,
|
||||||
|
duplicateColumn,
|
||||||
|
removeColumn,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -488,6 +495,7 @@ export const Table = (props) => {
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="left"
|
placement="left"
|
||||||
|
flip={true}
|
||||||
rootClose={isRootCloseEnabled}
|
rootClose={isRootCloseEnabled}
|
||||||
overlay={renderColumnPopover(item, index)}
|
overlay={renderColumnPopover(item, index)}
|
||||||
onToggle={(show) => handleToggleColumnPopover(index, show)}
|
onToggle={(show) => handleToggleColumnPopover(index, show)}
|
||||||
|
|
@ -554,9 +562,27 @@ export const Table = (props) => {
|
||||||
</div>
|
</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: (
|
children: (
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<div className="row g-2">
|
<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 TagsTypeIcon } from './TagsTypeIcon';
|
||||||
export { default as RadioTypeIcon } from './RadioTypeIcon';
|
export { default as RadioTypeIcon } from './RadioTypeIcon';
|
||||||
export { default as RatingTypeIcon } 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;
|
return modifiedColumn;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -62,6 +72,16 @@ export const useColumnManager = ({ component, paramUpdated, currentState }) => {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
await deleteEvents({ ref }, 'table_column');
|
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]
|
[component, paramUpdated, deleteEvents]
|
||||||
);
|
);
|
||||||
|
|
@ -75,6 +95,9 @@ export const useColumnManager = ({ component, paramUpdated, currentState }) => {
|
||||||
typeProp: 'columnType',
|
typeProp: 'columnType',
|
||||||
nonEditableTypes: ['link', 'image'],
|
nonEditableTypes: ['link', 'image'],
|
||||||
namePrefix: 'new_column',
|
namePrefix: 'new_column',
|
||||||
|
defaultItemProps: {
|
||||||
|
includeKey: true,
|
||||||
|
},
|
||||||
onPropertyChange: handlePropertyChange,
|
onPropertyChange: handlePropertyChange,
|
||||||
onRemove: handleRemove,
|
onRemove: handleRemove,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
TagsTypeIcon,
|
TagsTypeIcon,
|
||||||
RadioTypeIcon,
|
RadioTypeIcon,
|
||||||
RatingTypeIcon,
|
RatingTypeIcon,
|
||||||
|
ButtonTypeIcon,
|
||||||
} from './_assets';
|
} from './_assets';
|
||||||
|
|
||||||
export const getColumnIcon = (columnType) => {
|
export const getColumnIcon = (columnType) => {
|
||||||
|
|
@ -57,6 +58,8 @@ export const getColumnIcon = (columnType) => {
|
||||||
return TagsTypeIcon;
|
return TagsTypeIcon;
|
||||||
case 'rating':
|
case 'rating':
|
||||||
return RatingTypeIcon;
|
return RatingTypeIcon;
|
||||||
|
case 'button':
|
||||||
|
return ButtonTypeIcon;
|
||||||
default:
|
default:
|
||||||
return null;
|
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 { JsonColumn } from './adapters/JsonColumnAdapter';
|
||||||
export { MarkdownColumn } from './adapters/MarkdownColumnAdapter';
|
export { MarkdownColumn } from './adapters/MarkdownColumnAdapter';
|
||||||
export { HTMLColumn } from './adapters/HtmlColumnAdapter';
|
export { HTMLColumn } from './adapters/HtmlColumnAdapter';
|
||||||
|
export { ButtonColumnGroup } from './adapters/ButtonColumnGroupAdapter';
|
||||||
|
|
||||||
// Deprecated columns not moved to shared renderers
|
// Deprecated columns not moved to shared renderers
|
||||||
export { ToggleColumn } from './Toggle';
|
export { ToggleColumn } from './Toggle';
|
||||||
|
|
|
||||||
|
|
@ -57,15 +57,18 @@ export const TableRow = ({
|
||||||
data-cy={`${generateCypressDataCy(componentName)}-row-${virtualRow.index}`}
|
data-cy={`${generateCypressDataCy(componentName)}-row-${virtualRow.index}`}
|
||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => {
|
{row.getVisibleCells().map((cell) => {
|
||||||
|
const isButtonColumn = cell.column.columnDef?.meta?.columnType === 'button';
|
||||||
const cellStyles = {
|
const cellStyles = {
|
||||||
backgroundColor: getResolvedValue(cell.column.columnDef?.meta?.cellBackgroundColor ?? 'inherit', {
|
backgroundColor: getResolvedValue(cell.column.columnDef?.meta?.cellBackgroundColor ?? 'inherit', {
|
||||||
rowData: row.original,
|
rowData: row.original,
|
||||||
cellValue: cell.getValue(),
|
cellValue: cell.getValue(),
|
||||||
}),
|
}),
|
||||||
justifyContent: determineJustifyContentValue(cell.column.columnDef?.meta?.horizontalAlignment),
|
justifyContent: isButtonColumn
|
||||||
|
? undefined
|
||||||
|
: determineJustifyContentValue(cell.column.columnDef?.meta?.horizontalAlignment),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
textAlign: cell.column.columnDef?.meta?.horizontalAlignment,
|
textAlign: isButtonColumn ? undefined : cell.column.columnDef?.meta?.horizontalAlignment,
|
||||||
width: cell.column.getSize(),
|
width: cell.column.getSize(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -79,10 +82,15 @@ export const TableRow = ({
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={cell.id}
|
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}
|
style={cellStyles}
|
||||||
className={cx('table-cell td', {
|
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-left-actions': cell.column.id === 'leftActions',
|
||||||
'has-right-actions': cell.column.id === 'rightActions',
|
'has-right-actions': cell.column.id === 'rightActions',
|
||||||
'table-text-align-center': cell.column.columnDef?.meta?.horizontalAlignment === 'center',
|
'table-text-align-center': cell.column.columnDef?.meta?.horizontalAlignment === 'center',
|
||||||
|
|
@ -130,8 +138,9 @@ export const TableRow = ({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`td-container ${cell.column.columnDef?.meta?.columnType === 'image' && 'jet-table-image-column h-100'
|
className={`td-container ${
|
||||||
} ${cell.column.columnDef?.meta?.columnType !== 'image' && `w-100 h-100`}`}
|
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())}
|
{flexRender(cell.column?.columnDef?.cell, cell.getContext())}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
JsonColumn,
|
JsonColumn,
|
||||||
MarkdownColumn,
|
MarkdownColumn,
|
||||||
HTMLColumn,
|
HTMLColumn,
|
||||||
|
ButtonColumnGroup,
|
||||||
// Deprecated columns
|
// Deprecated columns
|
||||||
TagsColumn,
|
TagsColumn,
|
||||||
RadioColumn,
|
RadioColumn,
|
||||||
|
|
@ -23,8 +24,57 @@ import {
|
||||||
RatingColumn,
|
RatingColumn,
|
||||||
} from '../_components/DataTypes';
|
} from '../_components/DataTypes';
|
||||||
import useTableStore from '../_stores/tableStore';
|
import useTableStore from '../_stores/tableStore';
|
||||||
|
import { normalizeButtonEvent } from './normalizeButtonEvent';
|
||||||
import SelectSearch from 'react-select-search';
|
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({
|
export default function generateColumnsData({
|
||||||
columnProperties,
|
columnProperties,
|
||||||
columnSizes,
|
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:
|
default:
|
||||||
return cellValue || '';
|
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
|
// Add sorting configuration for specific column types
|
||||||
if (columnType === 'number') {
|
if (columnType === 'number') {
|
||||||
columnDef.sortingFn = (rowA, rowB, columnId) => {
|
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') {
|
if (eventName === 'onCalendarEventSelect') {
|
||||||
const { id, calendarEvent } = options;
|
const { id, calendarEvent } = options;
|
||||||
setExposedValue(id, 'selectedEvent', calendarEvent);
|
setExposedValue(id, 'selectedEvent', calendarEvent);
|
||||||
|
|
@ -943,12 +957,23 @@ export const createEventsSlice = (set, get) => ({
|
||||||
}
|
}
|
||||||
case 'switch-page': {
|
case 'switch-page': {
|
||||||
try {
|
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) {
|
if (!pageId) {
|
||||||
throw new Error('No page ID provided');
|
throw new Error('Either pageId or pageHandle must be provided');
|
||||||
}
|
}
|
||||||
const { switchPage } = get();
|
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 || [];
|
const queryParams = event.queryParams || [];
|
||||||
if (page.restricted && mode !== 'edit') {
|
if (page.restricted && mode !== 'edit') {
|
||||||
toast.error('Access to this page is restricted. Contact admin to know more.');
|
toast.error('Access to this page is restricted. Contact admin to know more.');
|
||||||
|
|
|
||||||
|
|
@ -1207,6 +1207,7 @@
|
||||||
z-index: 999 !important;
|
z-index: 999 !important;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
max-height: 85vh !important;
|
max-height: 85vh !important;
|
||||||
|
background-color: var(--cc-surface1-surface);
|
||||||
|
|
||||||
.popover-body {
|
.popover-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1214,8 +1215,10 @@
|
||||||
padding: 16px 0px 16px !important;
|
padding: 16px 0px 16px !important;
|
||||||
gap: 16px !important;
|
gap: 16px !important;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
background-color: var(--slate1) !important;
|
// background-color: var(--slate1) !important;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(85vh - 40px);
|
||||||
|
|
||||||
.optional-properties-when-editable-true {
|
.optional-properties-when-editable-true {
|
||||||
// background-color: var(--slate3);
|
// background-color: var(--slate3);
|
||||||
|
|
@ -1258,8 +1261,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover-header {
|
.popover-header {
|
||||||
padding: 4px 0 0 0 !important;
|
padding: 8px 16px 0px 16px !important;
|
||||||
background-color: var(--slate3);
|
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 {
|
.active-column-tab {
|
||||||
border-bottom: 1px solid var(--indigo9);
|
border-bottom: 1px solid var(--indigo9);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue