ToolJet/frontend/src/Editor/Inspector/Components/Table.jsx
Sherfin Shamsudeen 642c5caa71
Feature/multi page applications (Task ID - CU-2h1bfvw) (#4729)
* Add routes for multi-page apps

* Modify Editor, Viewer and Inspector to accept new app structure

* Show a page selector on left side bar

* Align component deletion logic with new app schema

* Make subcontainer work with multi-page apps

* Load components state properly in viewer

* Use UUID instead of handle for pages

* Display sidebar on viewer to switch pages

* Add proper URL suffixing for pages in viewer

* Add action to switch page

* Revert translation file back to its pre-existing linting

* Fix bug that caused modal to not open/close

* Add support for query params in page switch

* Fix the issue that caused navigation to fail while accessed via slug

* Add missing SwitchPage file

* Add support for page level variables

* Add migration to convert existing apps to new schema

* Add rollback for converting multi-page definitions back to single-page

* Fix migration for multi-page apps

* Adapt import/export service for multi-pages

* [improvements] Multi-page applications (#4755)

* UI updates for page selector popup card

* delete page

* delete page check: if only one page exits

* switch to home page if the selected page is removed

* adds and switch to new page

* updating page name

* updates to home page and starting page

* handle updating the home page when home page is deleted

* search box for filtering pages and minor style updates for the page handler card

* header search box style fixes

* for creating a new page, page handle needs to be unique

* seperating into smaller components

* updated pinned icon for page selector styles and settinf styles

* Leftsidebar header ui component

* handle dark theme

* page handle ui and dark theme fixes for page menu

* page handler edit modal

* pinned state and update pinned state for menu options triggered

* dark theme fixes for edit modal

* handle on update should not be empty or prev

* page handler updater

* added loading state for saving

* handles cancels

* fixes slug ui

* fixes crash for older app versions

* updates the query params when handle gets an update

* update homePage to homePageId

* removes console.log

* go back to the popover for modal close

* fixes: Difficult to select page

* fixes: Difficult to select the three-dot menu

* fixes: on visiting the root url, navigate to homepage on viewer

* adds tooltip for url

* updates the page selector sidebar with sync with query manager

* refactor and cleanup

* refactor and cleanup

* Compute component state when page is switched

* modal should not close on click outside

* disable save button if there is not change in the page handle input

* should show/hide page menu when hovered

* page icon

* updates delete icon for disabled state

* query manager should always be on top of page selector

* checks if homePage key exists in pages def

* updates page handler menu

* updates the clear icon

* page handler menu position

* page handler menu position

* handle icon

* alert msg

* global settings handler for updating viewer page navigation

* show/hode page navigation for viewer

* info text for toggle

* Multipages:with sortable list [DnD] (#4783)

* applied sortable list

* on sort updates the definitions

* fixies: app crash for dnd

* viwer: canvas width should be 100% when navigation drawer is disbaled

* fixes: homepage/startpage  reload

* clean up

Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com>

* Multipage UI viewer (#4801)

* new ui changes for viewer pages

* fixes postions for debugger and datasources popover

* removes console.log

* Multipage : hide page and unhide page feature (#4803)

* adds: ability to hide pages

* hides pages in viewer

* unhide page

* hide icon

* allow accessing hidden pages from url

* add: duplicate page (#4802)

* add: duplicate page

* do not copy the  same references from the original page

* page name and page handler should be unique for duplicate pages too

* Add support for on-page-load events

* Add icon from page settings menu item

* Convert existing templates to multi-page schema

* error logs for page level and app level errors (#4842)

* Adapt comments feature for multi-pages

* [Bugfix] multipage - page menu interactions (#4844)

* fixes: menu popup interaction

* fixes: on modal input focus, we switch the page

* Adapt multi-player to multi-pages

* Add editingPageId to ymap

* Log self, others and editor props in real-time avatar generation

* Save editing page id to appDef

* Add editingPageId to presence in RealtimeCursors

* adds no results ui for empty search results (#4869)

* page icon updated (#4870)

* fixes:Version switching crashes if the target version does not contain the current page (#4868)

* Remove unnecessary setting of editingPageId on ymap

* Remove unnecessary console.log

* [Bugfix] Multipages: widget inspector event popover unmounts (#4887)

* introduced a local state for events

* cleaned up inspector.jsx

* fixes: table widget inspector event accordion

* Do not run switchPage twice when viewer is loaded

* Preview should open the currently editing page

* Properly place navigation and canvas in viewer

* Update app definition whenever event manager changes are made

* Add support for browser back and forward button in multi-pages

* Rename handleBackButton to handlePageSwitchingBasedOnURLparam

* Add support for cut/copy/paste and clone

* Fix the crash caused by boxShadow

* Add support for background colors in viewer in multi-pages

* Run queries to be run on load on viewer, in multi-pages

* Fix issue that caused inspector popovers to collapse

* resolves workspace vars in viewer mode (#4892)

* Multipage : Navigation for Mobile-ui (#4814)

* refactored to components

* burger menu for mobile ui

* merge conflict fix for hidden pages

* hamburger menu positioned in the header

* viewer header reafctored

* viewer mobile page manu styles

* handles dark theme

* mobile menu with dark mode toggle in the footer

* components are moved to page level, handle for mobile layout

* style fixes

* removing unwanted code block

* dark theme fixes

* style fixes

* fixes: events are sortable (#4895)

* fixes: events are sortable

* Remove uneccesarily repeated call of setEvents in EventManager

Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com>

* renamed settings to Event handlers (#4898)

* updates the page setting title to Page Events

* temp commit

* Add support for setting max width in percentage

* fixes: paramUpdates for boxes: 🙌🏻

* [Bugfix] Multipage - viewer canvas dark theme (#4897)

* fixes: darktheme bg for viewer canvas

* reverts canvas size

* Fix for inspector bouncing back to previous values

* resolves pages variables in pythong and js transformation (#4905)

* csa support to event manager for pages (#4907)

* Add support for setting canvas width in percentages

* Persist page level variables across page switches

* latest definitions is merged with the current appdef (#4914)

* latest definitions is merged with the current appdef

* mutating the local obj

* cleanup

* iterate through pages for new versions are created

Co-authored-by: Arpit <arpitnath42@gmail.com>
2022-12-08 17:51:09 +05:30

1121 lines
47 KiB
JavaScript

import React from 'react';
import Accordion from '@/_ui/Accordion';
import { renderElement } from '../Utils';
import { computeActionName, resolveReferences } from '@/_helpers/utils';
// eslint-disable-next-line import/no-unresolved
import SortableList, { SortableItem } from 'react-easy-sort';
import arrayMove from 'array-move';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { Color } from '../Elements/Color';
import SelectSearch, { fuzzySearch } from 'react-select-search';
import { v4 as uuidv4 } from 'uuid';
import { EventManager } from '../EventManager';
import { CodeHinter } from '../../CodeBuilder/CodeHinter';
import { withTranslation } from 'react-i18next';
class TableComponent extends React.Component {
constructor(props) {
super(props);
const {
dataQueries,
component,
paramUpdated,
componentMeta,
eventUpdated,
eventOptionUpdated,
components,
currentState,
} = props;
this.state = {
dataQueries,
component,
paramUpdated,
componentMeta,
eventUpdated,
eventOptionUpdated,
components,
currentState,
actionPopOverRootClose: true,
showPopOver: false,
popOverRootCloseBlockers: [],
};
}
componentDidMount() {
const {
dataQueries,
component,
paramUpdated,
componentMeta,
eventUpdated,
eventOptionUpdated,
components,
currentState,
} = this.props;
this.setState({
dataQueries,
component,
paramUpdated,
componentMeta,
eventUpdated,
eventOptionUpdated,
components,
currentState,
});
}
onActionButtonPropertyChanged = (index, property, value) => {
const actions = this.props.component.component.definition.properties.actions;
actions.value[index][property] = value;
this.props.paramUpdated({ name: 'actions' }, 'value', actions.value, 'properties');
};
actionButtonEventsChanged = (events, index) => {
let actions = this.props.component.component.definition.properties.actions.value;
actions[index]['events'] = events;
this.props.paramUpdated({ name: 'actions' }, 'value', actions, 'properties');
};
actionButtonEventUpdated = (event, value, extraData) => {
const actions = this.props.component.component.definition.properties.actions;
const index = extraData.index;
let newValues = actions.value;
newValues[index][event.name] = {
actionId: value,
};
this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties');
};
actionButtonEventOptionUpdated = (event, option, value, extraData) => {
const actions = this.props.component.component.definition.properties.actions;
const index = extraData.index;
let newValues = actions.value;
const options = newValues[index][event.name].options;
newValues[index][event.name].options = {
...options,
[option]: value,
};
this.props.paramUpdated({ name: 'actions' }, 'value', newValues, 'properties');
};
columnEventChanged = (columnForWhichEventsAreChanged, events) => {
const columns = this.props.component.component.definition.properties.columns.value;
const newColumns = columns.map((column) => {
if (column.id === columnForWhichEventsAreChanged.id) {
const newColumn = { ...column, events };
return newColumn;
} else {
return column;
}
});
this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties');
};
setColumnPopoverRootCloseBlocker(key, isBlocking) {
if (isBlocking) {
this.setState((prev) => ({ popOverRootCloseBlockers: [...prev.popOverRootCloseBlockers, key] }));
} else {
this.setState((prev) => ({ popOverRootCloseBlockers: prev.popOverRootCloseBlockers.filter((b) => b !== key) }));
}
}
columnPopover = (column, index) => {
const timeZoneOptions = [
{ name: 'UTC', value: 'Etc/UTC' },
{ name: '-12:00', value: 'Etc/GMT+12' },
{ name: '-11:00', value: 'Etc/GMT+11' },
{ name: '-10:00', value: 'Pacific/Honolulu' },
{ name: '-09:00', value: 'America/Anchorage' },
{ name: '-08:00', value: 'America/Santa_Isabel' },
{ name: '-07:00', value: 'America/Chihuahua' },
{ name: '-06:00', value: 'America/Guatemala' },
{ name: '-05:00', value: 'America/Bogota' },
{ name: '-05:00', value: 'America/New_York' },
{ name: '-04:30', value: 'America/Caracas' },
{ name: '-04:00', value: 'America/Halifax' },
{ name: '-03:30', value: 'America/St_Johns' },
{ name: '-03:00', value: 'America/Sao_Paulo' },
{ name: '-02:00', value: 'Etc/GMT+2' },
{ name: '-01:00', value: 'Atlantic/Cape_Verde' },
{ name: '+00:00', value: 'Africa/Casablanca' },
{ name: '+01:00', value: 'Europe/Berlin' },
{ name: '+02:00', value: 'Europe/Istanbul' },
{ name: '+03:00', value: 'Asia/Baghdad' },
{ name: '+04:00', value: 'Europe/Moscow' },
{ name: '+04:30', value: 'Asia/Kabul' },
{ name: '+05:00', value: 'Asia/Tashkent' },
{ name: '+05:30', value: 'Asia/Colombo' },
{ name: '+05:45', value: 'Asia/Kathmandu' },
{ name: '+06:00', value: 'Asia/Almaty' },
{ name: '+06:30', value: 'Asia/Yangon' },
{ name: '+07:00', value: 'Asia/Bangkok' },
{ name: '+08:00', value: 'Asia/Krasnoyarsk' },
{ name: '+09:00', value: 'Asia/Seoul' },
{ name: '+09:30', value: 'Australia/Darwin' },
{ name: '+10:00', value: 'Australia/Hobart' },
{ name: '+11:00', value: 'Asia/Vladivostok' },
{ name: '+12:00', value: 'Etc/GMT-12' },
{ name: '+13:00', value: 'Pacific/Tongatapu' },
];
return (
<Popover id="popover-basic-2" className={`${this.props.darkMode && 'popover-dark-themed theme-dark'} shadow`}>
<Popover.Content>
<div className="field mb-2" data-cy={`dropdown-column-type`}>
<label data-cy={`label-column-type`} className="form-label">
{this.props.t('widget.Table.columnType', 'Column type')}
</label>
<SelectSearch
className={`${this.props.darkMode ? 'select-search-dark' : 'select-search'}`}
options={[
{ name: 'Default', value: 'default' },
{ name: 'String', value: 'string' },
{ name: 'Number', value: 'number' },
{ name: 'Text', value: 'text' },
{ name: 'Badge', value: 'badge' },
{ name: 'Multiple badges', value: 'badges' },
{ name: 'Tags', value: 'tags' },
{ name: 'Dropdown', value: 'dropdown' },
{ name: 'Radio', value: 'radio' },
{ name: 'Multiselect', value: 'multiselect' },
{ name: 'Toggle switch', value: 'toggle' },
{ name: 'Date Picker', value: 'datepicker' },
{ name: 'Image', value: 'image' },
]}
value={column.columnType}
search={true}
closeOnSelect={true}
onChange={(value) => {
this.onColumnItemChange(index, 'columnType', value);
}}
filterOptions={fuzzySearch}
placeholder={this.props.t('globals.select', 'Select') + '...'}
/>
</div>
<div className="field mb-2">
<label data-cy={`label-column-name`} className="form-label">
{this.props.t('widget.Table.columnName', 'Column name')}
</label>
<input
data-cy={`input-column-name`}
type="text"
className="form-control text-field"
onBlur={(e) => {
e.stopPropagation();
this.onColumnItemChange(index, 'name', e.target.value);
}}
defaultValue={column.name}
/>
</div>
{(column.columnType === 'string' || column.columnType === undefined || column.columnType === 'default') && (
<div data-cy={`input-overflow`} className="field mb-2">
<label data-cy={`label-overflow`} className="form-label">
{this.props.t('widget.Table.overflow', 'Overflow')}
</label>
<SelectSearch
className={`${this.props.darkMode ? 'select-search-dark' : 'select-search'}`}
options={[
{ name: 'Wrap', value: 'wrap' },
{ name: 'Scroll', value: 'scroll' },
{ name: 'Hide', value: 'hide' },
]}
value={column.textWrap}
search={true}
closeOnSelect={true}
onChange={(value) => {
this.onColumnItemChange(index, 'textWrap', value);
}}
filterOptions={fuzzySearch}
placeholder={this.props.t('globals.select', 'Select') + '...'}
/>
</div>
)}
<div data-cy={`label-and-input-key`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.key', 'key')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.key}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={column.name}
onChange={(value) => this.onColumnItemChange(index, 'key', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'key')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('tableKey', showing);
}}
/>
</div>
{(column.columnType === 'string' || column.columnType === undefined || column.columnType === 'default') && (
<div>
<div data-cy={`label-and-input-text-color`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.textColor', 'Text color')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.textColor}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'Text color of the cell'}
onChange={(value) => this.onColumnItemChange(index, 'textColor', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'textColor')}
fieldMeta={column}
component={this.state.component}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('textColor', showing);
}}
/>
</div>
{column.isEditable && (
<div>
<div data-cy={`header-validation`} className="hr-text">
{this.props.t('widget.Table.validation', 'Validation')}
</div>
<div data-cy={`input-and-label-regex`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.regex', 'Regex')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.regex}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'regex', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'regex')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('regex', showing);
}}
/>
</div>
<div data-cy={`input-and-label-min-length`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.minLength', 'Min length')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.minLength}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'minLength', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'minLength')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('minLength', showing);
}}
/>
</div>
<div data-cy={`input-and-label-max-length`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.maxLength', 'Max length')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.maxLength}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'maxLength', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'maxLength')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('maxLength', showing);
}}
/>
</div>
<div data-cy={`input-and-label-customo-role`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.customRule', 'Custom rule')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.customRule}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'customRule', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'customRule')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('customRule', showing);
}}
/>
</div>
</div>
)}
</div>
)}
{column.columnType === 'number' && column.isEditable && (
<div>
<div className="hr-text">{this.props.t('widget.Table.validation', 'Validation')}</div>
<div data-cy={`input-and-label-min-value`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.minValue', 'Min value')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.minLength}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'minValue', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'minValue')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('minValue', showing);
}}
/>
</div>
<div data-cy={`input-and-label-max-value`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.maxValue', 'Max value')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.maxLength}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'maxValue', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'maxValue')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('maxValue', showing);
}}
/>
</div>
</div>
)}
{column.columnType === 'toggle' && (
<div>
<div className="field mb-2">
<Color
param={{ name: 'Active color' }}
paramType="properties"
componentMeta={{ properties: { color: { displayName: 'Active color' } } }}
definition={{ value: column.activeColor || '#3c92dc' }}
onChange={(name, value, color) => this.onColumnItemChange(index, 'activeColor', color)}
/>
</div>
<EventManager
component={{
component: {
definition: {
events: column.events ?? [],
},
},
}}
componentMeta={{ events: { onChange: { displayName: 'On change' } } }}
currentState={this.props.currentState}
dataQueries={this.props.dataQueries}
components={this.props.components}
eventsChanged={(events) => this.columnEventChanged(column, events)}
apps={this.props.apps}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('event-manager', showing);
}}
pages={this.props.pages}
/>
</div>
)}
{(column.columnType === 'dropdown' ||
column.columnType === 'multiselect' ||
column.columnType === 'badge' ||
column.columnType === 'badges' ||
column.columnType === 'radio') && (
<div>
<div data-cy={`input-and-label-values`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.values', 'Values')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.values}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'{{[1, 2, 3]}}'}
onChange={(value) => this.onColumnItemChange(index, 'values', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'values')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('values', showing);
}}
/>
</div>
<div data-cy={`input-and-label-labels`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.labels', 'Labels')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.labels}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'{{["one", "two", "three"]}}'}
onChange={(value) => this.onColumnItemChange(index, 'labels', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'labels')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('labels', showing);
}}
/>
</div>
</div>
)}
{column.columnType === 'dropdown' && (
<>
{column.isEditable && (
<div>
<div data-cy={`header-validations`} className="hr-text">
{this.props.t('widget.Table.validation', 'Validation')}
</div>
<div data-cy={`input-and-label-custom-rule`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.customRule', 'Custom Rule')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.customRule}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'customRule', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'customRule')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('customRule', showing);
}}
/>
</div>
</div>
)}
</>
)}
<div data-cy={`input-and-label-cell-bg-color`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.cellBgColor', 'Cell Background Color')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.cellBackgroundColor ?? 'inherit'}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'cellBackgroundColor', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'cellBackgroundColor')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('cellBackgroundColor', showing);
}}
/>
</div>
{column.columnType === 'datepicker' && (
<div>
<label data-cy={`label-date-display-format`} className="form-label">
{this.props.t('widget.Table.dateDisplayformat', 'Date Display Format')}
</label>
<div data-cy={`input-date-display-format`} className="field mb-2">
<CodeHinter
currentState={this.props.currentState}
initialValue={column.dateFormat}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={'DD-MM-YYYY'}
onChange={(value) => this.onColumnItemChange(index, 'dateFormat', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'dateFormat')}
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('dateFormat', showing);
}}
/>
</div>
<label data-cy={`label-date-parse-format`} className="form-label">
{this.props.t('widget.Table.dateParseformat', 'Date Parse Format')}
</label>
<div className="field mb-2">
<input
data-cy={`input-date-parse-format`}
type="text"
className="form-control text-field"
onChange={(e) => {
e.stopPropagation();
this.onColumnItemChange(index, 'parseDateFormat', e.target.value);
}}
defaultValue={column.parseDateFormat}
placeholder={'DD-MM-YYYY'}
/>
</div>
<label data-cy={`label-parse-timezone`} className="form-label">
Parse in timezone
</label>
<div data-cy={`input-parse-timezone`} className="field mb-2">
<SelectSearch
className={`${this.props.darkMode ? 'select-search-dark' : 'select-search'}`}
options={timeZoneOptions}
value={column.timeZoneValue}
search={true}
closeOnSelect={true}
onChange={(value) => {
this.onColumnItemChange(index, 'timeZoneValue', value);
}}
filterOptions={fuzzySearch}
placeholder="Select.."
/>
</div>
<label data-cy={`label-display-time-zone`} className="form-label">
Display in timezone
</label>
<div ata-cy={`input-display-time-zone`} className="field mb-2">
<SelectSearch
className={`${this.props.darkMode ? 'select-search-dark' : 'select-search'}`}
options={timeZoneOptions}
value={column.timeZoneDisplay}
search={true}
closeOnSelect={true}
onChange={(value) => {
this.onColumnItemChange(index, 'timeZoneDisplay', value);
}}
filterOptions={fuzzySearch}
placeholder="Select.."
/>
</div>
<div className="field mb-2">
<div className="form-check form-switch my-2">
<input
data-cy={`toggle-show-time`}
className="form-check-input"
type="checkbox"
onClick={() => {
this.onColumnItemChange(index, 'isTimeChecked', !column.isTimeChecked);
}}
checked={column.isTimeChecked}
/>
<span data-cy={`label-show-time`} className="form-check-label">
{this.props.t('widget.Table.showTime', 'show time')}
</span>
</div>
</div>
</div>
)}
{column.columnType === 'image' && (
<>
<div data-cy={`input-and-label-border-radius`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.borderRadius', 'Border radius')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.borderRadius}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'borderRadius', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'borderRadius')}
/>
</div>
<div data-cy={`input-and-label-width`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.width', 'Width')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.width}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'width', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'width')}
/>
</div>
<div data-cy={`input-and-label-height`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.height', 'Height')}</label>
<CodeHinter
currentState={this.props.currentState}
initialValue={column.height}
theme={this.props.darkMode ? 'monokai' : 'default'}
mode="javascript"
lineNumbers={false}
placeholder={''}
onChange={(value) => this.onColumnItemChange(index, 'height', value)}
componentName={this.getPopoverFieldSource(column.columnType, 'height')}
/>
</div>
<div data-cy={`input-and-label-object-fit`} className="field mb-2">
<label className="form-label">{this.props.t('widget.Table.objectFit', 'Object fit')}</label>
<SelectSearch
className={`${this.props.darkMode ? 'select-search-dark' : 'select-search'}`}
options={[
{ name: 'Cover', value: 'cover' },
{ name: 'Contain', value: 'contain' },
{ name: 'Fill', value: 'fill' },
]}
value={column.objectFit}
search={true}
closeOnSelect={true}
onChange={(value) => {
this.onColumnItemChange(index, 'objectFit', value);
}}
filterOptions={fuzzySearch}
placeholder={this.props.t('Select') + '...'}
/>
</div>
</>
)}
{column.columnType !== 'image' && (
<div className="form-check form-switch my-4">
<input
data-cy={`toggle-make-editable`}
className="form-check-input"
type="checkbox"
onClick={() => this.onColumnItemChange(index, 'isEditable', !column.isEditable)}
checked={column.isEditable}
/>
<span data-cy={`label-make-editable`} className="form-check-label">
{this.props.t('widget.Table.makeEditable', 'make editable')}
</span>
</div>
)}
</Popover.Content>
</Popover>
);
};
actionPopOver = (action, index) => {
const dummyComponentForActionButton = {
component: {
definition: {
events: this.props.component.component.definition.properties.actions.value[index].events || [],
},
},
};
return (
<Popover id="popover-basic" className={`${this.props.darkMode && 'popover-dark-themed theme-dark'} shadow`}>
<Popover.Content>
<div className="field mb-2">
<label data-cy={`label-action-button-text`} className="form-label">
{this.props.t('widget.Table.buttonText', 'Button Text')}
</label>
<input
data-cy={`action-button-text-input-field`}
type="text"
className="form-control text-field"
onChange={(e) => {
e.stopPropagation();
this.onActionButtonPropertyChanged(index, 'buttonText', e.target.value);
}}
value={action.buttonText}
/>
</div>
<div className="field mb-2" data-cy={`dropdown-action-button-position`}>
<label data-cy={`label-action-button-position`} className="form-label">
{this.props.t('widget.Table.buttonPosition', 'Button Position')}
</label>
<SelectSearch
className={`${this.props.darkMode ? 'select-search-dark' : 'select-search'}`}
options={[
{ name: 'Left', value: 'left' },
{ name: 'Right', value: 'right' },
]}
value={action.position ?? 'right'}
search={false}
closeOnSelect={true}
onChange={(value) => {
this.onActionButtonPropertyChanged(index, 'position', value);
}}
filterOptions={fuzzySearch}
placeholder="Select position"
/>
</div>
<Color
param={{ name: 'actionButtonBackgroundColor' }}
paramType="properties"
componentMeta={this.state.componentMeta}
definition={{ value: action.backgroundColor }}
onChange={(name, value, color) => this.onActionButtonPropertyChanged(index, 'backgroundColor', color)}
cyLabel={`action-button-bg`}
/>
<Color
param={{ name: 'actionButtonTextColor' }}
paramType="properties"
componentMeta={this.state.componentMeta}
definition={{ value: action.textColor }}
onChange={(name, value, color) => this.onActionButtonPropertyChanged(index, 'textColor', color)}
cyLabel={`action-button-text`}
/>
<EventManager
component={dummyComponentForActionButton}
componentMeta={{ events: { onClick: { displayName: 'On click' } } }}
currentState={this.state.currentState}
dataQueries={this.props.dataQueries}
components={this.props.components}
eventsChanged={(events) => this.actionButtonEventsChanged(events, index)}
apps={this.props.apps}
popOverCallback={(showing) => {
this.setState({ actionPopOverRootClose: !showing });
this.setState({ showPopOver: showing });
}}
pages={this.props.pages}
/>
<button className="btn btn-sm btn-outline-danger mt-2 col" onClick={() => this.removeAction(index)}>
{this.props.t('widget.Table.remove', 'Remove')}
</button>
</Popover.Content>
</Popover>
);
};
actionButton(action, index) {
return (
<OverlayTrigger
trigger="click"
placement="left"
rootClose={this.state.actionPopOverRootClose}
overlay={this.actionPopOver(action, index)}
onToggle={(showing) => this.setState({ showPopOver: showing })}
>
<div className={`card p-2 mb-1 ${this.props.darkMode ? 'bg-secondary' : 'bg-light'}`} role="button">
<div className={`row ${this.props.darkMode ? '' : 'bg-light'}`}>
<div className="col-auto">
<div
data-cy={`action-button-${String(action.buttonText ?? '')
.toLowerCase()
.replace(/\s+/g, '-')}-${String(index ?? '')}`}
className="text"
>
{action.buttonText}
</div>
</div>
</div>
</div>
</OverlayTrigger>
);
}
onSortEnd = (oldIndex, newIndex) => {
const columns = this.props.component.component.definition.properties.columns;
const newColumns = arrayMove(columns.value, oldIndex, newIndex);
this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties');
};
generateNewColumnName = (columns) => {
let found = false;
let columnName = '';
let currentNumber = 1;
while (!found) {
columnName = `new_column${currentNumber}`;
if (columns.find((column) => column.name === columnName) === undefined) {
found = true;
}
currentNumber += 1;
}
return columnName;
};
addNewColumn = () => {
const columns = this.props.component.component.definition.properties.columns;
const newValue = columns.value;
newValue.push({ name: this.generateNewColumnName(columns.value), id: uuidv4() });
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
};
addNewAction = () => {
const actions = this.props.component.component.definition.properties.actions;
const newValue = actions ? actions.value : [];
newValue.push({ name: computeActionName(actions), buttonText: 'Button', events: [] });
this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties');
};
removeAction = (index) => {
const newValue = this.props.component.component.definition.properties.actions.value;
newValue.splice(index, 1);
this.props.paramUpdated({ name: 'actions' }, 'value', newValue, 'properties');
};
onColumnItemChange = (index, item, value) => {
const columns = this.props.component.component.definition.properties.columns;
const column = columns.value[index];
column[item] = value;
const newColumns = columns.value;
newColumns[index] = column;
this.props.paramUpdated({ name: 'columns' }, 'value', newColumns, 'properties');
};
removeColumn = (index) => {
const columns = this.props.component.component.definition.properties.columns;
const newValue = columns.value;
const removedColumns = newValue.splice(index, 1);
this.props.paramUpdated({ name: 'columns' }, 'value', newValue, 'properties');
const existingcolumnDeletionHistory =
this.props.component.component.definition.properties.columnDeletionHistory?.value ?? [];
const newcolumnDeletionHistory = [
...existingcolumnDeletionHistory,
...removedColumns.map((column) => column.key || column.name),
];
this.props.paramUpdated({ name: 'columnDeletionHistory' }, 'value', newcolumnDeletionHistory, 'properties');
};
getPopoverFieldSource = (column, field) =>
`widget/${this.props.component.component.name}/${column ?? 'default'}::${field}`;
render() {
const { dataQueries, component, paramUpdated, componentMeta, components, currentState, darkMode } = this.props;
const columns = component.component.definition.properties.columns;
const actions = component.component.definition.properties.actions || { value: [] };
if (!component.component.definition.properties.displaySearchBox)
paramUpdated({ name: 'displaySearchBox' }, 'value', true, 'properties');
const displaySearchBox = component.component.definition.properties.displaySearchBox.value;
const displayServerSideFilter = component.component.definition.properties.showFilterButton?.value
? resolveReferences(component.component.definition.properties.showFilterButton?.value, currentState)
: false;
const displayServerSideSearch = component.component.definition.properties.displaySearchBox?.value
? resolveReferences(component.component.definition.properties.displaySearchBox?.value, currentState)
: false;
const serverSidePagination = component.component.definition.properties.serverSidePagination?.value
? resolveReferences(component.component.definition.properties.serverSidePagination?.value, currentState)
: false;
const clientSidePagination = component.component.definition.properties.clientSidePagination?.value
? resolveReferences(component.component.definition.properties.clientSidePagination?.value, currentState)
: false;
const enabledSort = component.component.definition.properties.enabledSort?.value
? resolveReferences(component.component.definition.properties.enabledSort?.value, currentState)
: true;
const renderCustomElement = (param, paramType = 'properties') => {
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
};
let items = [];
items.push({
title: 'Properties',
children: renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
'data',
'properties',
currentState,
components,
darkMode
),
});
items.push({
title: 'Columns',
children: (
<div>
<div className="col-auto text-right mb-3">
<button
data-cy={`button-add-column`}
onClick={this.addNewColumn}
className="btn btn-sm border-0 font-weight-normal padding-2 col-auto color-primary inspector-add-button"
>
{this.props.t('widget.Table.addColumn', '+ Add column')}
</button>
</div>
<SortableList onSortEnd={this.onSortEnd} className="w-100" draggedItemClassName="dragged">
{columns.value.map((item, index) => (
<div className={`card p-2 column-sort-row mb-1 ${this.props.darkMode ? '' : 'bg-light'}`} key={index}>
<OverlayTrigger
trigger="click"
placement="left"
rootClose={this.state.popOverRootCloseBlockers.length === 0}
overlay={this.columnPopover(item, index)}
>
<div className={`row ${this.props.darkMode ? '' : 'bg-light'}`} role="button">
<div className="col-auto">
<SortableItem key={item.name}>
<svg
data-cy={`draggable-handle-column-${item.name}`}
width="8"
height="14"
viewBox="0 0 8 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.666667 1.66667C0.666667 2.03486 0.965143 2.33333 1.33333 2.33333C1.70152 2.33333 2 2.03486 2 1.66667C2 1.29848 1.70152 1 1.33333 1C0.965143 1 0.666667 1.29848 0.666667 1.66667Z"
stroke="#8092AC"
strokeWidth="1.33333"
/>
<path
d="M5.99992 1.66667C5.99992 2.03486 6.2984 2.33333 6.66659 2.33333C7.03478 2.33333 7.33325 2.03486 7.33325 1.66667C7.33325 1.29848 7.03478 1 6.66659 1C6.2984 1 5.99992 1.29848 5.99992 1.66667Z"
stroke="#8092AC"
strokeWidth="1.33333"
/>
<path
d="M0.666667 7.00001C0.666667 7.3682 0.965143 7.66668 1.33333 7.66668C1.70152 7.66668 2 7.3682 2 7.00001C2 6.63182 1.70152 6.33334 1.33333 6.33334C0.965143 6.33334 0.666667 6.63182 0.666667 7.00001Z"
stroke="#8092AC"
strokeWidth="1.33333"
/>
<path
d="M5.99992 7.00001C5.99992 7.3682 6.2984 7.66668 6.66659 7.66668C7.03478 7.66668 7.33325 7.3682 7.33325 7.00001C7.33325 6.63182 7.03478 6.33334 6.66659 6.33334C6.2984 6.33334 5.99992 6.63182 5.99992 7.00001Z"
stroke="#8092AC"
strokeWidth="1.33333"
/>
<path
d="M0.666667 12.3333C0.666667 12.7015 0.965143 13 1.33333 13C1.70152 13 2 12.7015 2 12.3333C2 11.9651 1.70152 11.6667 1.33333 11.6667C0.965143 11.6667 0.666667 11.9651 0.666667 12.3333Z"
stroke="#8092AC"
strokeWidth="1.33333"
/>
<path
d="M5.99992 12.3333C5.99992 12.7015 6.2984 13 6.66659 13C7.03478 13 7.33325 12.7015 7.33325 12.3333C7.33325 11.9651 7.03478 11.6667 6.66659 11.6667C6.2984 11.6667 5.99992 11.9651 5.99992 12.3333Z"
stroke="#8092AC"
strokeWidth="1.33333"
/>
</svg>
</SortableItem>
</div>
<div className="col">
<div data-cy={`column-${item.name}`} className="text">
{item.name}
</div>
</div>
<div className="col-auto">
<svg
data-cy={`button-delete-${item.name}`}
onClick={() => this.removeColumn(index)}
width="10"
height="16"
viewBox="0 0 10 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 13.8333C0 14.75 0.75 15.5 1.66667 15.5H8.33333C9.25 15.5 10 14.75 10 13.8333V3.83333H0V13.8333ZM1.66667 5.5H8.33333V13.8333H1.66667V5.5ZM7.91667 1.33333L7.08333 0.5H2.91667L2.08333 1.33333H0V3H10V1.33333H7.91667Z"
fill="#8092AC"
/>
</svg>
</div>
</div>
</OverlayTrigger>
</div>
))}
</SortableList>
</div>
),
});
items.push({
title: 'Action buttons',
children: (
<div className="field mb-2 mt-2">
<div className="row g-2">
<div className="text-right mb-3">
<button
data-cy="button-add-new-action-button"
onClick={this.addNewAction}
className="btn btn-sm border-0 font-weight-normal padding-2 col-auto color-primary inspector-add-button"
>
{this.props.t('widget.Table.addButton', '+ Add button')}
</button>
</div>
</div>
<div>{actions.value.map((action, index) => this.actionButton(action, index))}</div>
{actions.value.length === 0 && (
<div className="text-center">
<small data-cy="message-no-action-button" className="color-disabled">
{this.props.t('widget.Table.noActionMessage', "This table doesn't have any action buttons")}
</small>
</div>
)}
</div>
),
});
const options = [
'serverSidePagination',
...(serverSidePagination ? ['enablePrevButton'] : []),
...(serverSidePagination ? ['enableNextButton'] : []),
...(serverSidePagination ? ['totalRecords'] : []),
...(clientSidePagination && !serverSidePagination ? ['rowsPerPage'] : []),
'enabledSort',
...(enabledSort ? ['serverSideSort'] : []),
'showDownloadButton',
'showFilterButton',
...(displayServerSideFilter ? ['serverSideFilter'] : []),
'showBulkUpdateActions',
'showBulkSelector',
'highlightSelectedRow',
'hideColumnSelectorButton',
];
let renderOptions = [];
!serverSidePagination && options.splice(1, 0, 'clientSidePagination');
options.map((option) => renderOptions.push(renderCustomElement(option)));
const conditionalOptions = [
{ name: 'displaySearchBox', condition: displaySearchBox },
{ name: 'serverSideSearch', condition: displayServerSideSearch },
{ name: 'loadingState', condition: true },
];
conditionalOptions.map(({ name, condition }) => {
if (condition) renderOptions.push(renderCustomElement(name));
});
items.push({
title: 'Options',
children: renderOptions,
});
items.push({
title: 'Events',
isOpen: true,
children: (
<EventManager
component={component}
componentMeta={componentMeta}
currentState={currentState}
dataQueries={dataQueries}
components={components}
eventsChanged={this.props.eventsChanged}
apps={this.props.apps}
pages={this.props.pages}
/>
),
});
items.push({
title: 'Layout',
isOpen: true,
children: (
<>
{renderElement(
component,
componentMeta,
this.props.layoutPropertyChanged,
dataQueries,
'showOnDesktop',
'others',
currentState,
components
)}
{renderElement(
component,
componentMeta,
this.props.layoutPropertyChanged,
dataQueries,
'showOnMobile',
'others',
currentState,
components
)}
</>
),
});
return <Accordion items={items} />;
}
}
export const Table = withTranslation()(TableComponent);